mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2025-02-07 05:03:32 +01:00
550b9782e1
using portmapping with hosting while using traversal server (which is possible by checking the option while under "direct connect" and flipping back to traversal server) causes dolphin to request a mapping to external port 0. In UPnP a mapping to external port 0 is actually the wildcard, which means that connection requests on all external ports (that are not otherwise mapped) will be forwarded to the client. Additionally it seems like using port mapping with traversal server is probably not expected behavior, as the option checkbox disappears when traversal server is used.
2490 lines
71 KiB
C++
2490 lines
71 KiB
C++
// Copyright 2010 Dolphin Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
#include "Core/NetPlayServer.h"
|
|
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <cstddef>
|
|
#include <cstdio>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <type_traits>
|
|
#include <unordered_set>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include <fmt/format.h>
|
|
|
|
#include "Common/CommonPaths.h"
|
|
#include "Common/ENet.h"
|
|
#include "Common/FileUtil.h"
|
|
#include "Common/HttpRequest.h"
|
|
#include "Common/Logging/Log.h"
|
|
#include "Common/MsgHandler.h"
|
|
#include "Common/SFMLHelper.h"
|
|
#include "Common/StringUtil.h"
|
|
#include "Common/UPnP.h"
|
|
#include "Common/Version.h"
|
|
|
|
#include "Core/ActionReplay.h"
|
|
#include "Core/Boot/Boot.h"
|
|
#include "Core/Config/GraphicsSettings.h"
|
|
#include "Core/Config/MainSettings.h"
|
|
#include "Core/Config/NetplaySettings.h"
|
|
#include "Core/Config/SYSCONFSettings.h"
|
|
#include "Core/Config/SessionSettings.h"
|
|
#include "Core/ConfigLoaders/GameConfigLoader.h"
|
|
#include "Core/ConfigManager.h"
|
|
#include "Core/GeckoCode.h"
|
|
#include "Core/GeckoCodeConfig.h"
|
|
#include "Core/HW/EXI/EXI.h"
|
|
#include "Core/HW/EXI/EXI_Device.h"
|
|
#ifdef HAS_LIBMGBA
|
|
#include "Core/HW/GBACore.h"
|
|
#endif
|
|
#include "Core/HW/GCMemcard/GCMemcard.h"
|
|
#include "Core/HW/GCMemcard/GCMemcardDirectory.h"
|
|
#include "Core/HW/GCMemcard/GCMemcardRaw.h"
|
|
#include "Core/HW/Sram.h"
|
|
#include "Core/HW/WiiSave.h"
|
|
#include "Core/HW/WiiSaveStructs.h"
|
|
#include "Core/HW/WiimoteEmu/DesiredWiimoteState.h"
|
|
#include "Core/HW/WiimoteEmu/WiimoteEmu.h"
|
|
#include "Core/HW/WiimoteReal/WiimoteReal.h"
|
|
#include "Core/IOS/ES/ES.h"
|
|
#include "Core/IOS/FS/FileSystem.h"
|
|
#include "Core/IOS/IOS.h"
|
|
#include "Core/IOS/Uids.h"
|
|
#include "Core/NetPlayClient.h" //for NetPlayUI
|
|
#include "Core/NetPlayCommon.h"
|
|
#include "Core/SyncIdentifier.h"
|
|
|
|
#include "DiscIO/Enums.h"
|
|
#include "DiscIO/RiivolutionPatcher.h"
|
|
|
|
#include "InputCommon/ControllerEmu/ControlGroup/Attachments.h"
|
|
#include "InputCommon/GCPadStatus.h"
|
|
#include "InputCommon/InputConfig.h"
|
|
|
|
#include "UICommon/GameFile.h"
|
|
|
|
#if !defined(_WIN32)
|
|
#include <sys/socket.h>
|
|
#include <sys/types.h>
|
|
#ifdef __HAIKU__
|
|
#define _BSD_SOURCE
|
|
#include <bsd/ifaddrs.h>
|
|
#elif !defined ANDROID
|
|
#include <ifaddrs.h>
|
|
#endif
|
|
#include <arpa/inet.h>
|
|
#endif
|
|
|
|
namespace NetPlay
|
|
{
|
|
NetPlayServer::~NetPlayServer()
|
|
{
|
|
if (is_connected)
|
|
{
|
|
m_do_loop = false;
|
|
m_chunked_data_event.Set();
|
|
m_chunked_data_complete_event.Set();
|
|
if (m_chunked_data_thread.joinable())
|
|
m_chunked_data_thread.join();
|
|
m_thread.join();
|
|
enet_host_destroy(m_server);
|
|
|
|
if (Common::g_MainNetHost.get() == m_server)
|
|
{
|
|
Common::g_MainNetHost.release();
|
|
}
|
|
|
|
if (m_traversal_client)
|
|
{
|
|
Common::g_TraversalClient->m_Client = nullptr;
|
|
Common::ReleaseTraversalClient();
|
|
}
|
|
}
|
|
|
|
#ifdef USE_UPNP
|
|
Common::UPnP::StopPortmapping();
|
|
#endif
|
|
}
|
|
|
|
// called from ---GUI--- thread
|
|
NetPlayServer::NetPlayServer(const u16 port, const bool forward_port, NetPlayUI* dialog,
|
|
const NetTraversalConfig& traversal_config)
|
|
: m_dialog(dialog)
|
|
{
|
|
//--use server time
|
|
if (enet_initialize() != 0)
|
|
{
|
|
PanicAlertFmtT("Enet Didn't Initialize");
|
|
}
|
|
|
|
m_pad_map.fill(0);
|
|
m_gba_config.fill({});
|
|
m_wiimote_map.fill(0);
|
|
|
|
if (traversal_config.use_traversal)
|
|
{
|
|
if (!Common::EnsureTraversalClient(traversal_config.traversal_host,
|
|
traversal_config.traversal_port, port))
|
|
{
|
|
return;
|
|
}
|
|
|
|
Common::g_TraversalClient->m_Client = this;
|
|
m_traversal_client = Common::g_TraversalClient.get();
|
|
|
|
m_server = Common::g_MainNetHost.get();
|
|
|
|
if (Common::g_TraversalClient->HasFailed())
|
|
Common::g_TraversalClient->ReconnectToServer();
|
|
}
|
|
else
|
|
{
|
|
ENetAddress serverAddr;
|
|
serverAddr.host = ENET_HOST_ANY;
|
|
serverAddr.port = port;
|
|
m_server = enet_host_create(&serverAddr, 10, CHANNEL_COUNT, 0, 0);
|
|
if (m_server != nullptr)
|
|
{
|
|
m_server->mtu = std::min(m_server->mtu, NetPlay::MAX_ENET_MTU);
|
|
m_server->intercept = Common::ENet::InterceptCallback;
|
|
}
|
|
|
|
SetupIndex();
|
|
}
|
|
if (m_server != nullptr)
|
|
{
|
|
is_connected = true;
|
|
m_do_loop = true;
|
|
m_thread = std::thread(&NetPlayServer::ThreadFunc, this);
|
|
m_target_buffer_size = 5;
|
|
m_chunked_data_thread = std::thread(&NetPlayServer::ChunkedDataThreadFunc, this);
|
|
|
|
#ifdef USE_UPNP
|
|
if (forward_port && !traversal_config.use_traversal)
|
|
Common::UPnP::TryPortmapping(port);
|
|
#endif
|
|
}
|
|
}
|
|
|
|
static PlayerId* PeerPlayerId(ENetPeer* peer)
|
|
{
|
|
return static_cast<PlayerId*>(peer->data);
|
|
}
|
|
|
|
static void ClearPeerPlayerId(ENetPeer* peer)
|
|
{
|
|
if (peer->data)
|
|
{
|
|
delete PeerPlayerId(peer);
|
|
peer->data = nullptr;
|
|
}
|
|
}
|
|
|
|
void NetPlayServer::SetupIndex()
|
|
{
|
|
if (!Config::Get(Config::NETPLAY_USE_INDEX) || Config::Get(Config::NETPLAY_INDEX_NAME).empty() ||
|
|
Config::Get(Config::NETPLAY_INDEX_REGION).empty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
NetPlaySession session;
|
|
|
|
session.name = Config::Get(Config::NETPLAY_INDEX_NAME);
|
|
session.region = Config::Get(Config::NETPLAY_INDEX_REGION);
|
|
session.has_password = !Config::Get(Config::NETPLAY_INDEX_PASSWORD).empty();
|
|
session.method = m_traversal_client ? "traversal" : "direct";
|
|
session.game_id = m_selected_game_name.empty() ? "UNKNOWN" : m_selected_game_name;
|
|
session.player_count = static_cast<int>(m_players.size());
|
|
session.in_game = m_is_running;
|
|
session.port = GetPort();
|
|
|
|
if (m_traversal_client)
|
|
{
|
|
if (!m_traversal_client->IsConnected())
|
|
return;
|
|
|
|
session.server_id = std::string(Common::g_TraversalClient->GetHostID().data(), 8);
|
|
}
|
|
else
|
|
{
|
|
Common::HttpRequest request;
|
|
// ENet does not support IPv6, so IPv4 has to be used
|
|
request.UseIPv4();
|
|
Common::HttpRequest::Response response =
|
|
request.Get("https://ip.dolphin-emu.org/", {{"X-Is-Dolphin", "1"}});
|
|
|
|
if (!response.has_value())
|
|
return;
|
|
|
|
session.server_id = std::string(response->begin(), response->end());
|
|
}
|
|
|
|
session.EncryptID(Config::Get(Config::NETPLAY_INDEX_PASSWORD));
|
|
|
|
bool success = m_index.Add(session);
|
|
if (m_dialog != nullptr)
|
|
m_dialog->OnIndexAdded(success, success ? "" : m_index.GetLastError());
|
|
|
|
m_index.SetErrorCallback([this] {
|
|
if (m_dialog != nullptr)
|
|
m_dialog->OnIndexRefreshFailed(m_index.GetLastError());
|
|
});
|
|
}
|
|
|
|
// called from ---NETPLAY--- thread
|
|
void NetPlayServer::ThreadFunc()
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "NetPlayServer starting.");
|
|
|
|
while (m_do_loop)
|
|
{
|
|
// update pings every so many seconds
|
|
if ((m_ping_timer.ElapsedMs() > 1000) || m_update_pings)
|
|
{
|
|
// only used as an identifier, not time value, so truncation is fine
|
|
m_ping_key = static_cast<u32>(Common::Timer::NowMs());
|
|
|
|
sf::Packet spac;
|
|
spac << MessageID::Ping;
|
|
spac << m_ping_key;
|
|
|
|
m_ping_timer.Start();
|
|
SendToClients(spac);
|
|
|
|
m_index.SetPlayerCount(static_cast<int>(m_players.size()));
|
|
m_index.SetGame(m_selected_game_name);
|
|
m_index.SetInGame(m_is_running);
|
|
|
|
m_update_pings = false;
|
|
}
|
|
|
|
ENetEvent netEvent;
|
|
int net;
|
|
if (m_traversal_client)
|
|
m_traversal_client->HandleResends();
|
|
net = enet_host_service(m_server, &netEvent, 1000);
|
|
while (!m_async_queue.Empty())
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "Processing async queue event.");
|
|
{
|
|
std::lock_guard lkp(m_crit.players);
|
|
INFO_LOG_FMT(NETPLAY, "Locked player mutex.");
|
|
auto& e = m_async_queue.Front();
|
|
if (e.target_mode == TargetMode::Only)
|
|
{
|
|
if (m_players.find(e.target_pid) != m_players.end())
|
|
Send(m_players.at(e.target_pid).socket, e.packet, e.channel_id);
|
|
}
|
|
else
|
|
{
|
|
SendToClients(e.packet, e.target_pid, e.channel_id);
|
|
}
|
|
}
|
|
INFO_LOG_FMT(NETPLAY, "Processing async queue event done.");
|
|
m_async_queue.Pop();
|
|
}
|
|
if (net > 0)
|
|
{
|
|
switch (netEvent.type)
|
|
{
|
|
case ENET_EVENT_TYPE_CONNECT:
|
|
{
|
|
// Actual client initialization is deferred to the receive event, so here
|
|
// we'll just log the new connection.
|
|
INFO_LOG_FMT(NETPLAY, "Peer connected from: {:x}:{}", netEvent.peer->address.host,
|
|
netEvent.peer->address.port);
|
|
}
|
|
break;
|
|
case ENET_EVENT_TYPE_RECEIVE:
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "enet_host_service: receive event");
|
|
|
|
sf::Packet rpac;
|
|
rpac.append(netEvent.packet->data, netEvent.packet->dataLength);
|
|
|
|
if (!netEvent.peer->data)
|
|
{
|
|
// uninitialized client, we'll assume this is their initialization packet
|
|
ConnectionError error;
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "Initializing peer {:x}:{}", netEvent.peer->address.host,
|
|
netEvent.peer->address.port);
|
|
std::lock_guard lkg(m_crit.game);
|
|
error = OnConnect(netEvent.peer, rpac);
|
|
}
|
|
|
|
if (error != ConnectionError::NoError)
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "Error {} initializing peer {:x}:{}", u8(error),
|
|
netEvent.peer->address.host, netEvent.peer->address.port);
|
|
|
|
sf::Packet spac;
|
|
spac << error;
|
|
// don't need to lock, this client isn't in the client map
|
|
Send(netEvent.peer, spac);
|
|
|
|
ClearPeerPlayerId(netEvent.peer);
|
|
enet_peer_disconnect_later(netEvent.peer, 0);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
auto it = m_players.find(*PeerPlayerId(netEvent.peer));
|
|
Client& client = it->second;
|
|
if (OnData(rpac, client) != 0)
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "Invalid packet from client {}, disconnecting.", client.pid);
|
|
|
|
// if a bad packet is received, disconnect the client
|
|
std::lock_guard lkg(m_crit.game);
|
|
OnDisconnect(client);
|
|
|
|
ClearPeerPlayerId(netEvent.peer);
|
|
}
|
|
else
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "successfully handled packet from client {}", client.pid);
|
|
}
|
|
}
|
|
enet_packet_destroy(netEvent.packet);
|
|
}
|
|
break;
|
|
case ENET_EVENT_TYPE_DISCONNECT:
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "enet_host_service: disconnect event");
|
|
|
|
std::lock_guard lkg(m_crit.game);
|
|
if (!netEvent.peer->data)
|
|
{
|
|
ERROR_LOG_FMT(NETPLAY, "enet_host_service: no peer data");
|
|
break;
|
|
}
|
|
const auto player_id = *PeerPlayerId(netEvent.peer);
|
|
auto it = m_players.find(player_id);
|
|
if (it != m_players.end())
|
|
{
|
|
Client& client = it->second;
|
|
INFO_LOG_FMT(NETPLAY, "Disconnecting client {}.", client.pid);
|
|
OnDisconnect(client);
|
|
|
|
ClearPeerPlayerId(netEvent.peer);
|
|
}
|
|
else
|
|
{
|
|
ERROR_LOG_FMT(NETPLAY, "Invalid player {} to disconnect.", player_id);
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
ERROR_LOG_FMT(NETPLAY, "enet_host_service: unknown event type: {}", int(netEvent.type));
|
|
break;
|
|
}
|
|
}
|
|
else if (net == 0)
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "enet_host_service: no event occurred");
|
|
}
|
|
else
|
|
{
|
|
ERROR_LOG_FMT(NETPLAY, "enet_host_service error: {}", net);
|
|
}
|
|
}
|
|
|
|
INFO_LOG_FMT(NETPLAY, "NetPlayServer shutting down.");
|
|
|
|
// close listening socket and client sockets
|
|
for (auto& player_entry : m_players)
|
|
{
|
|
ClearPeerPlayerId(player_entry.second.socket);
|
|
enet_peer_disconnect(player_entry.second.socket, 0);
|
|
}
|
|
m_players.clear();
|
|
}
|
|
|
|
static void SendSyncIdentifier(sf::Packet& spac, const SyncIdentifier& sync_identifier)
|
|
{
|
|
// We cast here due to a potential long vs long long mismatch
|
|
spac << static_cast<sf::Uint64>(sync_identifier.dol_elf_size);
|
|
|
|
spac << sync_identifier.game_id;
|
|
spac << sync_identifier.revision;
|
|
spac << sync_identifier.disc_number;
|
|
spac << sync_identifier.is_datel;
|
|
|
|
for (const u8& x : sync_identifier.sync_hash)
|
|
spac << x;
|
|
}
|
|
|
|
// called from ---NETPLAY--- thread
|
|
ConnectionError NetPlayServer::OnConnect(ENetPeer* incoming_connection, sf::Packet& received_packet)
|
|
{
|
|
std::string netplay_version;
|
|
received_packet >> netplay_version;
|
|
if (netplay_version != Common::GetScmRevGitStr())
|
|
return ConnectionError::VersionMismatch;
|
|
|
|
if (m_is_running || m_start_pending)
|
|
return ConnectionError::GameRunning;
|
|
|
|
if (m_players.size() >= 255)
|
|
return ConnectionError::ServerFull;
|
|
|
|
Client new_player{};
|
|
new_player.pid = GiveFirstAvailableIDTo(incoming_connection);
|
|
new_player.socket = incoming_connection;
|
|
|
|
received_packet >> new_player.revision;
|
|
received_packet >> new_player.name;
|
|
|
|
if (StringUTF8CodePointCount(new_player.name) > MAX_NAME_LENGTH)
|
|
return ConnectionError::NameTooLong;
|
|
|
|
// Update time in milliseconds of no acknoledgment of
|
|
// sent packets before a connection is deemed disconnected
|
|
enet_peer_timeout(incoming_connection, 0, PEER_TIMEOUT.count(), PEER_TIMEOUT.count());
|
|
|
|
// force a ping on first netplay loop
|
|
m_update_pings = true;
|
|
|
|
AssignNewUserAPad(new_player);
|
|
|
|
// tell other players a new player joined
|
|
SendResponseToAllPlayers(MessageID::PlayerJoin, new_player.pid, new_player.name,
|
|
new_player.revision);
|
|
|
|
// tell new client they connected and their ID
|
|
SendResponseToPlayer(new_player, MessageID::ConnectionSuccessful, new_player.pid);
|
|
|
|
// tell new client the selected game
|
|
if (!m_selected_game_name.empty())
|
|
{
|
|
sf::Packet send_packet;
|
|
send_packet << MessageID::ChangeGame;
|
|
SendSyncIdentifier(send_packet, m_selected_game_identifier);
|
|
send_packet << m_selected_game_name;
|
|
Send(new_player.socket, send_packet);
|
|
}
|
|
|
|
if (!m_host_input_authority)
|
|
SendResponseToPlayer(new_player, MessageID::PadBuffer, m_target_buffer_size);
|
|
|
|
SendResponseToPlayer(new_player, MessageID::HostInputAuthority, m_host_input_authority);
|
|
|
|
for (const auto& existing_player : m_players)
|
|
{
|
|
SendResponseToPlayer(new_player, MessageID::PlayerJoin, existing_player.second.pid,
|
|
existing_player.second.name, existing_player.second.revision);
|
|
|
|
SendResponseToPlayer(new_player, MessageID::GameStatus, existing_player.second.pid,
|
|
static_cast<u8>(existing_player.second.game_status));
|
|
}
|
|
|
|
if (Config::Get(Config::NETPLAY_ENABLE_QOS))
|
|
new_player.qos_session = Common::QoSSession(new_player.socket);
|
|
|
|
{
|
|
std::lock_guard lkp(m_crit.players);
|
|
// add new player to list of players
|
|
m_players.emplace(*PeerPlayerId(new_player.socket), std::move(new_player));
|
|
// sync pad mappings with everyone
|
|
UpdatePadMapping();
|
|
UpdateGBAConfig();
|
|
UpdateWiimoteMapping();
|
|
}
|
|
|
|
return ConnectionError::NoError;
|
|
}
|
|
|
|
// called from ---NETPLAY--- thread
|
|
unsigned int NetPlayServer::OnDisconnect(const Client& player)
|
|
{
|
|
const PlayerId pid = player.pid;
|
|
|
|
if (m_is_running)
|
|
{
|
|
for (PlayerId& mapping : m_pad_map)
|
|
{
|
|
if (mapping == pid && pid != 1)
|
|
{
|
|
std::lock_guard lkg(m_crit.game);
|
|
m_is_running = false;
|
|
|
|
sf::Packet spac;
|
|
spac << MessageID::DisableGame;
|
|
// this thread doesn't need players lock
|
|
SendToClients(spac);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (m_start_pending)
|
|
{
|
|
ChunkedDataAbort();
|
|
m_dialog->OnGameStartAborted();
|
|
m_start_pending = false;
|
|
}
|
|
|
|
sf::Packet spac;
|
|
spac << MessageID::PlayerLeave;
|
|
spac << pid;
|
|
|
|
enet_peer_disconnect(player.socket, 0);
|
|
|
|
std::lock_guard lkp(m_crit.players);
|
|
auto it = m_players.find(player.pid);
|
|
if (it != m_players.end())
|
|
m_players.erase(it);
|
|
|
|
// alert other players of disconnect
|
|
SendToClients(spac);
|
|
|
|
for (size_t i = 0; i < m_pad_map.size(); ++i)
|
|
{
|
|
if (m_pad_map[i] == pid)
|
|
{
|
|
m_pad_map[i] = 0;
|
|
m_gba_config[i].enabled = false;
|
|
UpdatePadMapping();
|
|
UpdateGBAConfig();
|
|
}
|
|
}
|
|
|
|
for (PlayerId& mapping : m_wiimote_map)
|
|
{
|
|
if (mapping == pid)
|
|
{
|
|
mapping = 0;
|
|
UpdateWiimoteMapping();
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// called from ---GUI--- thread
|
|
PadMappingArray NetPlayServer::GetPadMapping() const
|
|
{
|
|
return m_pad_map;
|
|
}
|
|
|
|
GBAConfigArray NetPlayServer::GetGBAConfig() const
|
|
{
|
|
return m_gba_config;
|
|
}
|
|
|
|
PadMappingArray NetPlayServer::GetWiimoteMapping() const
|
|
{
|
|
return m_wiimote_map;
|
|
}
|
|
|
|
// called from ---GUI--- thread
|
|
void NetPlayServer::SetPadMapping(const PadMappingArray& mappings)
|
|
{
|
|
m_pad_map = mappings;
|
|
UpdatePadMapping();
|
|
}
|
|
|
|
// called from ---GUI--- thread
|
|
void NetPlayServer::SetGBAConfig(const GBAConfigArray& mappings, bool update_rom)
|
|
{
|
|
#ifdef HAS_LIBMGBA
|
|
m_gba_config = mappings;
|
|
if (update_rom)
|
|
{
|
|
for (size_t i = 0; i < m_gba_config.size(); ++i)
|
|
{
|
|
auto& config = m_gba_config[i];
|
|
if (!config.enabled)
|
|
continue;
|
|
std::string rom_path = Config::Get(Config::MAIN_GBA_ROM_PATHS[i]);
|
|
config.has_rom = HW::GBA::Core::GetRomInfo(rom_path.c_str(), config.hash, config.title);
|
|
}
|
|
}
|
|
#endif
|
|
UpdateGBAConfig();
|
|
}
|
|
|
|
// called from ---GUI--- thread
|
|
void NetPlayServer::SetWiimoteMapping(const PadMappingArray& mappings)
|
|
{
|
|
m_wiimote_map = mappings;
|
|
UpdateWiimoteMapping();
|
|
}
|
|
|
|
// called from ---GUI--- thread and ---NETPLAY--- thread
|
|
void NetPlayServer::UpdatePadMapping()
|
|
{
|
|
sf::Packet spac;
|
|
spac << MessageID::PadMapping;
|
|
for (PlayerId mapping : m_pad_map)
|
|
{
|
|
spac << mapping;
|
|
}
|
|
SendToClients(spac);
|
|
}
|
|
|
|
// called from ---GUI--- thread and ---NETPLAY--- thread
|
|
void NetPlayServer::UpdateGBAConfig()
|
|
{
|
|
sf::Packet spac;
|
|
spac << MessageID::GBAConfig;
|
|
for (const auto& config : m_gba_config)
|
|
{
|
|
spac << config.enabled << config.has_rom << config.title;
|
|
for (auto& data : config.hash)
|
|
spac << data;
|
|
}
|
|
SendToClients(spac);
|
|
}
|
|
|
|
// called from ---NETPLAY--- thread
|
|
void NetPlayServer::UpdateWiimoteMapping()
|
|
{
|
|
sf::Packet spac;
|
|
spac << MessageID::WiimoteMapping;
|
|
for (PlayerId mapping : m_wiimote_map)
|
|
{
|
|
spac << mapping;
|
|
}
|
|
SendToClients(spac);
|
|
}
|
|
|
|
// called from ---GUI--- thread and ---NETPLAY--- thread
|
|
void NetPlayServer::AdjustPadBufferSize(unsigned int size)
|
|
{
|
|
std::lock_guard lkg(m_crit.game);
|
|
|
|
m_target_buffer_size = size;
|
|
|
|
// not needed on clients with host input authority
|
|
if (!m_host_input_authority)
|
|
{
|
|
// tell clients to change buffer size
|
|
sf::Packet spac;
|
|
spac << MessageID::PadBuffer;
|
|
spac << m_target_buffer_size;
|
|
|
|
SendAsyncToClients(std::move(spac));
|
|
}
|
|
}
|
|
|
|
void NetPlayServer::SetHostInputAuthority(const bool enable)
|
|
{
|
|
std::lock_guard lkg(m_crit.game);
|
|
|
|
m_host_input_authority = enable;
|
|
|
|
// tell clients about the new value
|
|
sf::Packet spac;
|
|
spac << MessageID::HostInputAuthority;
|
|
spac << m_host_input_authority;
|
|
|
|
SendAsyncToClients(std::move(spac));
|
|
|
|
// resend pad buffer to clients when disabled
|
|
if (!m_host_input_authority)
|
|
AdjustPadBufferSize(m_target_buffer_size);
|
|
}
|
|
|
|
void NetPlayServer::SendAsync(sf::Packet&& packet, const PlayerId pid, const u8 channel_id)
|
|
{
|
|
{
|
|
std::lock_guard lkq(m_crit.async_queue_write);
|
|
m_async_queue.Push(AsyncQueueEntry{std::move(packet), pid, TargetMode::Only, channel_id});
|
|
}
|
|
Common::ENet::WakeupThread(m_server);
|
|
}
|
|
|
|
void NetPlayServer::SendAsyncToClients(sf::Packet&& packet, const PlayerId skip_pid,
|
|
const u8 channel_id)
|
|
{
|
|
{
|
|
std::lock_guard lkq(m_crit.async_queue_write);
|
|
m_async_queue.Push(
|
|
AsyncQueueEntry{std::move(packet), skip_pid, TargetMode::AllExcept, channel_id});
|
|
}
|
|
Common::ENet::WakeupThread(m_server);
|
|
}
|
|
|
|
void NetPlayServer::SendChunked(sf::Packet&& packet, const PlayerId pid, const std::string& title)
|
|
{
|
|
{
|
|
std::lock_guard lkq(m_crit.chunked_data_queue_write);
|
|
m_chunked_data_queue.Push(
|
|
ChunkedDataQueueEntry{std::move(packet), pid, TargetMode::Only, title});
|
|
}
|
|
m_chunked_data_event.Set();
|
|
}
|
|
|
|
void NetPlayServer::SendChunkedToClients(sf::Packet&& packet, const PlayerId skip_pid,
|
|
const std::string& title)
|
|
{
|
|
{
|
|
std::lock_guard lkq(m_crit.chunked_data_queue_write);
|
|
m_chunked_data_queue.Push(
|
|
ChunkedDataQueueEntry{std::move(packet), skip_pid, TargetMode::AllExcept, title});
|
|
}
|
|
m_chunked_data_event.Set();
|
|
}
|
|
|
|
// called from ---NETPLAY--- thread
|
|
unsigned int NetPlayServer::OnData(sf::Packet& packet, Client& player)
|
|
{
|
|
MessageID mid;
|
|
packet >> mid;
|
|
|
|
INFO_LOG_FMT(NETPLAY, "Got client message: {:x} from client {}", static_cast<u8>(mid),
|
|
player.pid);
|
|
|
|
// don't need lock because this is the only thread that modifies the players
|
|
// only need locks for writes to m_players in this thread
|
|
|
|
switch (mid)
|
|
{
|
|
case MessageID::ChatMessage:
|
|
{
|
|
std::string msg;
|
|
packet >> msg;
|
|
|
|
// send msg to other clients
|
|
sf::Packet spac;
|
|
spac << MessageID::ChatMessage;
|
|
spac << player.pid;
|
|
spac << msg;
|
|
|
|
SendToClients(spac, player.pid);
|
|
}
|
|
break;
|
|
|
|
case MessageID::ChunkedDataProgress:
|
|
{
|
|
u32 cid;
|
|
packet >> cid;
|
|
u64 progress = Common::PacketReadU64(packet);
|
|
|
|
m_dialog->SetChunkedProgress(player.pid, progress);
|
|
}
|
|
break;
|
|
|
|
case MessageID::ChunkedDataComplete:
|
|
{
|
|
u32 cid;
|
|
packet >> cid;
|
|
|
|
if (m_chunked_data_complete_count.find(cid) != m_chunked_data_complete_count.end())
|
|
{
|
|
m_chunked_data_complete_count[cid]++;
|
|
m_chunked_data_complete_event.Set();
|
|
}
|
|
}
|
|
break;
|
|
|
|
case MessageID::PadData:
|
|
{
|
|
// if this is pad data from the last game still being received, ignore it
|
|
if (player.current_game != m_current_game)
|
|
break;
|
|
|
|
sf::Packet spac;
|
|
spac << (m_host_input_authority ? MessageID::PadHostData : MessageID::PadData);
|
|
|
|
while (!packet.endOfPacket())
|
|
{
|
|
PadIndex map;
|
|
packet >> map;
|
|
|
|
// If the data is not from the correct player,
|
|
// then disconnect them.
|
|
if (m_pad_map.at(map) != player.pid)
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
GCPadStatus pad;
|
|
packet >> pad.button;
|
|
spac << map << pad.button;
|
|
if (!m_gba_config.at(map).enabled)
|
|
{
|
|
packet >> pad.analogA >> pad.analogB >> pad.stickX >> pad.stickY >> pad.substickX >>
|
|
pad.substickY >> pad.triggerLeft >> pad.triggerRight >> pad.isConnected;
|
|
|
|
spac << pad.analogA << pad.analogB << pad.stickX << pad.stickY << pad.substickX
|
|
<< pad.substickY << pad.triggerLeft << pad.triggerRight << pad.isConnected;
|
|
}
|
|
}
|
|
|
|
if (m_host_input_authority)
|
|
{
|
|
// Prevent crash before game stop if the golfer disconnects
|
|
if (m_current_golfer != 0 && m_players.find(m_current_golfer) != m_players.end())
|
|
Send(m_players.at(m_current_golfer).socket, spac);
|
|
}
|
|
else
|
|
{
|
|
SendToClients(spac, player.pid);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case MessageID::PadHostData:
|
|
{
|
|
// Kick player if they're not the golfer.
|
|
if (m_current_golfer != 0 && player.pid != m_current_golfer)
|
|
return 1;
|
|
|
|
sf::Packet spac;
|
|
spac << MessageID::PadData;
|
|
|
|
while (!packet.endOfPacket())
|
|
{
|
|
PadIndex map;
|
|
packet >> map;
|
|
|
|
GCPadStatus pad;
|
|
packet >> pad.button;
|
|
spac << map << pad.button;
|
|
if (!m_gba_config.at(map).enabled)
|
|
{
|
|
packet >> pad.analogA >> pad.analogB >> pad.stickX >> pad.stickY >> pad.substickX >>
|
|
pad.substickY >> pad.triggerLeft >> pad.triggerRight >> pad.isConnected;
|
|
|
|
spac << pad.analogA << pad.analogB << pad.stickX << pad.stickY << pad.substickX
|
|
<< pad.substickY << pad.triggerLeft << pad.triggerRight << pad.isConnected;
|
|
}
|
|
}
|
|
|
|
SendToClients(spac, player.pid);
|
|
}
|
|
break;
|
|
|
|
case MessageID::WiimoteData:
|
|
{
|
|
// if this is Wiimote data from the last game still being received, ignore it
|
|
if (player.current_game != m_current_game)
|
|
break;
|
|
|
|
sf::Packet spac;
|
|
spac << MessageID::WiimoteData;
|
|
|
|
while (!packet.endOfPacket())
|
|
{
|
|
PadIndex map;
|
|
packet >> map;
|
|
|
|
// If the data is not from the correct player,
|
|
// then disconnect them.
|
|
if (m_wiimote_map.at(map) != player.pid)
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
WiimoteEmu::SerializedWiimoteState pad;
|
|
packet >> pad.length;
|
|
if (pad.length > pad.data.size())
|
|
return 1;
|
|
for (size_t i = 0; i < pad.length; ++i)
|
|
packet >> pad.data[i];
|
|
|
|
spac << map;
|
|
spac << pad.length;
|
|
for (size_t i = 0; i < pad.length; ++i)
|
|
spac << pad.data[i];
|
|
}
|
|
|
|
SendToClients(spac, player.pid);
|
|
}
|
|
break;
|
|
|
|
case MessageID::GolfRequest:
|
|
{
|
|
PlayerId pid;
|
|
packet >> pid;
|
|
|
|
// Check if player ID is valid and sender isn't a spectator
|
|
if (!m_players.count(pid) || !PlayerHasControllerMapped(player.pid))
|
|
break;
|
|
|
|
if (m_host_input_authority && m_settings.golf_mode && m_pending_golfer == 0 &&
|
|
m_current_golfer != pid && PlayerHasControllerMapped(pid))
|
|
{
|
|
m_pending_golfer = pid;
|
|
|
|
sf::Packet spac;
|
|
spac << MessageID::GolfPrepare;
|
|
Send(m_players[pid].socket, spac);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case MessageID::GolfRelease:
|
|
{
|
|
if (m_pending_golfer == 0)
|
|
break;
|
|
|
|
sf::Packet spac;
|
|
spac << MessageID::GolfSwitch;
|
|
spac << m_pending_golfer;
|
|
SendToClients(spac);
|
|
}
|
|
break;
|
|
|
|
case MessageID::GolfAcquire:
|
|
{
|
|
if (m_pending_golfer == 0)
|
|
break;
|
|
|
|
m_current_golfer = m_pending_golfer;
|
|
m_pending_golfer = 0;
|
|
}
|
|
break;
|
|
|
|
case MessageID::GolfPrepare:
|
|
{
|
|
if (m_pending_golfer == 0)
|
|
break;
|
|
|
|
m_current_golfer = 0;
|
|
|
|
sf::Packet spac;
|
|
spac << MessageID::GolfSwitch;
|
|
spac << PlayerId{0};
|
|
SendToClients(spac);
|
|
}
|
|
break;
|
|
|
|
case MessageID::Pong:
|
|
{
|
|
// truncation (> ~49 days elapsed) should never happen here
|
|
const u32 ping = static_cast<u32>(m_ping_timer.ElapsedMs());
|
|
u32 ping_key = 0;
|
|
packet >> ping_key;
|
|
|
|
if (m_ping_key == ping_key)
|
|
{
|
|
player.ping = ping;
|
|
}
|
|
|
|
sf::Packet spac;
|
|
spac << MessageID::PlayerPingData;
|
|
spac << player.pid;
|
|
spac << player.ping;
|
|
|
|
SendToClients(spac);
|
|
}
|
|
break;
|
|
|
|
case MessageID::StartGame:
|
|
{
|
|
packet >> player.current_game;
|
|
}
|
|
break;
|
|
|
|
case MessageID::StopGame:
|
|
{
|
|
if (!m_is_running)
|
|
break;
|
|
|
|
m_is_running = false;
|
|
|
|
// tell clients to stop game
|
|
sf::Packet spac;
|
|
spac << MessageID::StopGame;
|
|
|
|
std::lock_guard lkp(m_crit.players);
|
|
SendToClients(spac);
|
|
}
|
|
break;
|
|
|
|
case MessageID::GameStatus:
|
|
{
|
|
SyncIdentifierComparison status;
|
|
packet >> status;
|
|
|
|
m_players[player.pid].game_status = status;
|
|
|
|
// send msg to other clients
|
|
sf::Packet spac;
|
|
spac << MessageID::GameStatus;
|
|
spac << player.pid;
|
|
spac << status;
|
|
|
|
SendToClients(spac);
|
|
}
|
|
break;
|
|
|
|
case MessageID::ClientCapabilities:
|
|
{
|
|
packet >> m_players[player.pid].has_ipl_dump;
|
|
packet >> m_players[player.pid].has_hardware_fma;
|
|
}
|
|
break;
|
|
|
|
case MessageID::PowerButton:
|
|
{
|
|
sf::Packet spac;
|
|
spac << MessageID::PowerButton;
|
|
SendToClients(spac, player.pid);
|
|
}
|
|
break;
|
|
|
|
case MessageID::TimeBase:
|
|
{
|
|
u64 timebase = Common::PacketReadU64(packet);
|
|
u32 frame;
|
|
packet >> frame;
|
|
|
|
if (m_desync_detected)
|
|
break;
|
|
|
|
std::vector<std::pair<PlayerId, u64>>& timebases = m_timebase_by_frame[frame];
|
|
timebases.emplace_back(player.pid, timebase);
|
|
if (timebases.size() >= m_players.size())
|
|
{
|
|
// we have all records for this frame
|
|
|
|
if (!std::all_of(timebases.begin(), timebases.end(), [&](std::pair<PlayerId, u64> pair) {
|
|
return pair.second == timebases[0].second;
|
|
}))
|
|
{
|
|
int pid_to_blame = 0;
|
|
for (auto pair : timebases)
|
|
{
|
|
if (std::all_of(timebases.begin(), timebases.end(), [&](std::pair<PlayerId, u64> other) {
|
|
return other.first == pair.first || other.second != pair.second;
|
|
}))
|
|
{
|
|
// we are the only outlier
|
|
pid_to_blame = pair.first;
|
|
break;
|
|
}
|
|
}
|
|
|
|
sf::Packet spac;
|
|
spac << MessageID::DesyncDetected;
|
|
spac << pid_to_blame;
|
|
spac << frame;
|
|
SendToClients(spac);
|
|
|
|
m_desync_detected = true;
|
|
}
|
|
m_timebase_by_frame.erase(frame);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case MessageID::GameDigestProgress:
|
|
{
|
|
int progress;
|
|
packet >> progress;
|
|
|
|
sf::Packet spac;
|
|
spac << MessageID::GameDigestProgress;
|
|
spac << player.pid;
|
|
spac << progress;
|
|
|
|
SendToClients(spac);
|
|
}
|
|
break;
|
|
|
|
case MessageID::GameDigestResult:
|
|
{
|
|
std::string result;
|
|
packet >> result;
|
|
|
|
sf::Packet spac;
|
|
spac << MessageID::GameDigestResult;
|
|
spac << player.pid;
|
|
spac << result;
|
|
|
|
SendToClients(spac);
|
|
}
|
|
break;
|
|
|
|
case MessageID::GameDigestError:
|
|
{
|
|
std::string error;
|
|
packet >> error;
|
|
|
|
sf::Packet spac;
|
|
spac << MessageID::GameDigestError;
|
|
spac << player.pid;
|
|
spac << error;
|
|
|
|
SendToClients(spac);
|
|
}
|
|
break;
|
|
|
|
case MessageID::SyncSaveData:
|
|
{
|
|
SyncSaveDataID sub_id;
|
|
packet >> sub_id;
|
|
|
|
INFO_LOG_FMT(NETPLAY, "Got client SyncSaveData message: {:x} from client {}", u8(sub_id),
|
|
player.pid);
|
|
|
|
switch (sub_id)
|
|
{
|
|
case SyncSaveDataID::Success:
|
|
{
|
|
if (m_start_pending)
|
|
{
|
|
m_save_data_synced_players++;
|
|
if (m_save_data_synced_players >= m_players.size() - 1)
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "SyncSaveData: All players synchronized. ({} >= {})",
|
|
m_save_data_synced_players, m_players.size() - 1);
|
|
m_dialog->AppendChat(Common::GetStringT("All players' saves synchronized."));
|
|
|
|
// Saves are synced, check if codes are as well and attempt to start the game
|
|
m_saves_synced = true;
|
|
CheckSyncAndStartGame();
|
|
}
|
|
else
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "SyncSaveData: Not all players synchronized. ({} < {})",
|
|
m_save_data_synced_players, m_players.size() - 1);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "SyncSaveData: Start not pending.");
|
|
}
|
|
}
|
|
break;
|
|
|
|
case SyncSaveDataID::Failure:
|
|
{
|
|
m_dialog->AppendChat(Common::FmtFormatT("{0} failed to synchronize.", player.name));
|
|
m_dialog->OnGameStartAborted();
|
|
ChunkedDataAbort();
|
|
m_start_pending = false;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
PanicAlertFmtT(
|
|
"Unknown SYNC_SAVE_DATA message with id:{0} received from player:{1} Kicking player!",
|
|
static_cast<u8>(sub_id), player.pid);
|
|
return 1;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case MessageID::SyncCodes:
|
|
{
|
|
// Receive Status of Code Sync
|
|
SyncCodeID sub_id;
|
|
packet >> sub_id;
|
|
|
|
INFO_LOG_FMT(NETPLAY, "Got client SyncCodes message: {:x} from client {}", u8(sub_id),
|
|
player.pid);
|
|
|
|
// Check If Code Sync was successful or not
|
|
switch (sub_id)
|
|
{
|
|
case SyncCodeID::Success:
|
|
{
|
|
if (m_start_pending)
|
|
{
|
|
if (++m_codes_synced_players >= m_players.size() - 1)
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "SyncCodes: All players synchronized. ({} >= {})",
|
|
m_codes_synced_players, m_players.size() - 1);
|
|
|
|
m_dialog->AppendChat(Common::GetStringT("All players' codes synchronized."));
|
|
|
|
// Codes are synced, check if saves are as well and attempt to start the game
|
|
m_codes_synced = true;
|
|
CheckSyncAndStartGame();
|
|
}
|
|
else
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "SyncCodes: Not all players synchronized. ({} < {})",
|
|
m_codes_synced_players, m_players.size() - 1);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "SyncCodes: Start not pending.");
|
|
}
|
|
}
|
|
break;
|
|
|
|
case SyncCodeID::Failure:
|
|
{
|
|
m_dialog->AppendChat(Common::FmtFormatT("{0} failed to synchronize codes.", player.name));
|
|
m_dialog->OnGameStartAborted();
|
|
m_start_pending = false;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
PanicAlertFmtT(
|
|
"Unknown SYNC_GECKO_CODES message with id:{0} received from player:{1} Kicking player!",
|
|
static_cast<u8>(sub_id), player.pid);
|
|
return 1;
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
PanicAlertFmtT("Unknown message with id:{0} received from player:{1} Kicking player!",
|
|
static_cast<u8>(mid), player.pid);
|
|
// unknown message, kick the client
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void NetPlayServer::OnTraversalStateChanged()
|
|
{
|
|
const Common::TraversalClient::State state = m_traversal_client->GetState();
|
|
|
|
if (Common::g_TraversalClient->GetHostID()[0] != '\0')
|
|
SetupIndex();
|
|
|
|
if (!m_dialog)
|
|
return;
|
|
|
|
if (state == Common::TraversalClient::State::Failure)
|
|
m_dialog->OnTraversalError(m_traversal_client->GetFailureReason());
|
|
|
|
m_dialog->OnTraversalStateChanged(state);
|
|
}
|
|
|
|
// called from ---GUI--- thread
|
|
void NetPlayServer::SendChatMessage(const std::string& msg)
|
|
{
|
|
sf::Packet spac;
|
|
spac << MessageID::ChatMessage;
|
|
spac << PlayerId{0}; // server ID always 0
|
|
spac << msg;
|
|
|
|
SendAsyncToClients(std::move(spac));
|
|
}
|
|
|
|
// called from ---GUI--- thread
|
|
bool NetPlayServer::ChangeGame(const SyncIdentifier& sync_identifier,
|
|
const std::string& netplay_name)
|
|
{
|
|
std::lock_guard lkg(m_crit.game);
|
|
|
|
INFO_LOG_FMT(NETPLAY, "Changing game to {} ({:02x}).", netplay_name,
|
|
fmt::join(sync_identifier.sync_hash, ""));
|
|
|
|
m_selected_game_identifier = sync_identifier;
|
|
m_selected_game_name = netplay_name;
|
|
|
|
// send changed game to clients
|
|
sf::Packet spac;
|
|
spac << MessageID::ChangeGame;
|
|
SendSyncIdentifier(spac, m_selected_game_identifier);
|
|
spac << m_selected_game_name;
|
|
|
|
SendAsyncToClients(std::move(spac));
|
|
|
|
return true;
|
|
}
|
|
|
|
// called from ---GUI--- thread
|
|
bool NetPlayServer::ComputeGameDigest(const SyncIdentifier& sync_identifier)
|
|
{
|
|
sf::Packet spac;
|
|
spac << MessageID::ComputeGameDigest;
|
|
SendSyncIdentifier(spac, sync_identifier);
|
|
|
|
SendAsyncToClients(std::move(spac));
|
|
|
|
return true;
|
|
}
|
|
|
|
// called from ---GUI--- thread
|
|
bool NetPlayServer::AbortGameDigest()
|
|
{
|
|
sf::Packet spac;
|
|
spac << MessageID::GameDigestAbort;
|
|
|
|
SendAsyncToClients(std::move(spac));
|
|
return true;
|
|
}
|
|
|
|
// called from ---GUI--- thread
|
|
bool NetPlayServer::SetupNetSettings()
|
|
{
|
|
const auto game = m_dialog->FindGameFile(m_selected_game_identifier);
|
|
if (game == nullptr)
|
|
{
|
|
ERROR_LOG_FMT(NETPLAY, "Game {:02x} not found in game list.",
|
|
fmt::join(m_selected_game_identifier.sync_hash, ""));
|
|
PanicAlertFmtT("Selected game doesn't exist in game list!");
|
|
return false;
|
|
}
|
|
|
|
INFO_LOG_FMT(NETPLAY, "Loading game settings for {:02x}.",
|
|
fmt::join(m_selected_game_identifier.sync_hash, ""));
|
|
|
|
NetPlay::NetSettings settings;
|
|
|
|
// Load GameINI so we can sync the settings from it
|
|
Config::AddLayer(
|
|
ConfigLoaders::GenerateGlobalGameConfigLoader(game->GetGameID(), game->GetRevision()));
|
|
Config::AddLayer(
|
|
ConfigLoaders::GenerateLocalGameConfigLoader(game->GetGameID(), game->GetRevision()));
|
|
|
|
// Copy all relevant settings
|
|
settings.cpu_thread = Config::Get(Config::MAIN_CPU_THREAD);
|
|
settings.cpu_core = Config::Get(Config::MAIN_CPU_CORE);
|
|
settings.enable_cheats = Config::Get(Config::MAIN_ENABLE_CHEATS);
|
|
settings.selected_language = Config::Get(Config::MAIN_GC_LANGUAGE);
|
|
settings.override_region_settings = Config::Get(Config::MAIN_OVERRIDE_REGION_SETTINGS);
|
|
settings.dsp_hle = Config::Get(Config::MAIN_DSP_HLE);
|
|
settings.dsp_enable_jit = Config::Get(Config::MAIN_DSP_JIT);
|
|
settings.ram_override_enable = Config::Get(Config::MAIN_RAM_OVERRIDE_ENABLE);
|
|
settings.mem1_size = Config::Get(Config::MAIN_MEM1_SIZE);
|
|
settings.mem2_size = Config::Get(Config::MAIN_MEM2_SIZE);
|
|
settings.fallback_region = Config::Get(Config::MAIN_FALLBACK_REGION);
|
|
settings.allow_sd_writes = Config::Get(Config::MAIN_ALLOW_SD_WRITES);
|
|
settings.oc_enable = Config::Get(Config::MAIN_OVERCLOCK_ENABLE);
|
|
settings.oc_factor = Config::Get(Config::MAIN_OVERCLOCK);
|
|
|
|
for (ExpansionInterface::Slot slot : ExpansionInterface::SLOTS)
|
|
{
|
|
ExpansionInterface::EXIDeviceType device;
|
|
if (slot == ExpansionInterface::Slot::SP1)
|
|
{
|
|
// There's no way the BBA is going to sync, disable it
|
|
device = ExpansionInterface::EXIDeviceType::None;
|
|
}
|
|
else
|
|
{
|
|
device = Config::Get(Config::GetInfoForEXIDevice(slot));
|
|
}
|
|
settings.exi_device[slot] = device;
|
|
}
|
|
|
|
settings.memcard_size_override = Config::Get(Config::MAIN_MEMORY_CARD_SIZE);
|
|
|
|
for (size_t i = 0; i < Config::SYSCONF_SETTINGS.size(); ++i)
|
|
{
|
|
std::visit(
|
|
[&](auto* info) {
|
|
static_assert(sizeof(info->GetDefaultValue()) <= sizeof(u32));
|
|
settings.sysconf_settings[i] = static_cast<u32>(Config::Get(*info));
|
|
},
|
|
Config::SYSCONF_SETTINGS[i].config_info);
|
|
}
|
|
|
|
settings.efb_access_enable = Config::Get(Config::GFX_HACK_EFB_ACCESS_ENABLE);
|
|
settings.bbox_enable = Config::Get(Config::GFX_HACK_BBOX_ENABLE);
|
|
settings.force_progressive = Config::Get(Config::GFX_HACK_FORCE_PROGRESSIVE);
|
|
settings.efb_to_texture_enable = Config::Get(Config::GFX_HACK_SKIP_EFB_COPY_TO_RAM);
|
|
settings.xfb_to_texture_enable = Config::Get(Config::GFX_HACK_SKIP_XFB_COPY_TO_RAM);
|
|
settings.disable_copy_to_vram = Config::Get(Config::GFX_HACK_DISABLE_COPY_TO_VRAM);
|
|
settings.immediate_xfb_enable = Config::Get(Config::GFX_HACK_IMMEDIATE_XFB);
|
|
settings.efb_emulate_format_changes = Config::Get(Config::GFX_HACK_EFB_EMULATE_FORMAT_CHANGES);
|
|
settings.safe_texture_cache_color_samples =
|
|
Config::Get(Config::GFX_SAFE_TEXTURE_CACHE_COLOR_SAMPLES);
|
|
settings.perf_queries_enable = Config::Get(Config::GFX_PERF_QUERIES_ENABLE);
|
|
settings.float_exceptions = Config::Get(Config::MAIN_FLOAT_EXCEPTIONS);
|
|
settings.divide_by_zero_exceptions = Config::Get(Config::MAIN_DIVIDE_BY_ZERO_EXCEPTIONS);
|
|
settings.fprf = Config::Get(Config::MAIN_FPRF);
|
|
settings.accurate_nans = Config::Get(Config::MAIN_ACCURATE_NANS);
|
|
settings.disable_icache = Config::Get(Config::MAIN_DISABLE_ICACHE);
|
|
settings.sync_on_skip_idle = Config::Get(Config::MAIN_SYNC_ON_SKIP_IDLE);
|
|
settings.sync_gpu = Config::Get(Config::MAIN_SYNC_GPU);
|
|
settings.sync_gpu_max_distance = Config::Get(Config::MAIN_SYNC_GPU_MAX_DISTANCE);
|
|
settings.sync_gpu_min_distance = Config::Get(Config::MAIN_SYNC_GPU_MIN_DISTANCE);
|
|
settings.sync_gpu_overclock = Config::Get(Config::MAIN_SYNC_GPU_OVERCLOCK);
|
|
settings.jit_follow_branch = Config::Get(Config::MAIN_JIT_FOLLOW_BRANCH);
|
|
settings.fast_disc_speed = Config::Get(Config::MAIN_FAST_DISC_SPEED);
|
|
settings.mmu = Config::Get(Config::MAIN_MMU);
|
|
settings.fastmem = Config::Get(Config::MAIN_FASTMEM);
|
|
settings.skip_ipl = Config::Get(Config::MAIN_SKIP_IPL) || !DoAllPlayersHaveIPLDump();
|
|
settings.load_ipl_dump = Config::Get(Config::SESSION_LOAD_IPL_DUMP) && DoAllPlayersHaveIPLDump();
|
|
settings.vertex_rounding = Config::Get(Config::GFX_HACK_VERTEX_ROUNDING);
|
|
settings.internal_resolution = Config::Get(Config::GFX_EFB_SCALE);
|
|
settings.efb_scaled_copy = Config::Get(Config::GFX_HACK_COPY_EFB_SCALED);
|
|
settings.fast_depth_calc = Config::Get(Config::GFX_FAST_DEPTH_CALC);
|
|
settings.enable_pixel_lighting = Config::Get(Config::GFX_ENABLE_PIXEL_LIGHTING);
|
|
settings.widescreen_hack = Config::Get(Config::GFX_WIDESCREEN_HACK);
|
|
settings.force_texture_filtering = Config::Get(Config::GFX_ENHANCE_FORCE_TEXTURE_FILTERING);
|
|
settings.max_anisotropy = Config::Get(Config::GFX_ENHANCE_MAX_ANISOTROPY);
|
|
settings.force_true_color = Config::Get(Config::GFX_ENHANCE_FORCE_TRUE_COLOR);
|
|
settings.disable_copy_filter = Config::Get(Config::GFX_ENHANCE_DISABLE_COPY_FILTER);
|
|
settings.disable_fog = Config::Get(Config::GFX_DISABLE_FOG);
|
|
settings.arbitrary_mipmap_detection = Config::Get(Config::GFX_ENHANCE_ARBITRARY_MIPMAP_DETECTION);
|
|
settings.arbitrary_mipmap_detection_threshold =
|
|
Config::Get(Config::GFX_ENHANCE_ARBITRARY_MIPMAP_DETECTION_THRESHOLD);
|
|
settings.enable_gpu_texture_decoding = Config::Get(Config::GFX_ENABLE_GPU_TEXTURE_DECODING);
|
|
settings.defer_efb_copies = Config::Get(Config::GFX_HACK_DEFER_EFB_COPIES);
|
|
settings.efb_access_tile_size = Config::Get(Config::GFX_HACK_EFB_ACCESS_TILE_SIZE);
|
|
settings.efb_access_defer_invalidation = Config::Get(Config::GFX_HACK_EFB_DEFER_INVALIDATION);
|
|
|
|
settings.savedata_load = Config::Get(Config::NETPLAY_SAVEDATA_LOAD);
|
|
settings.savedata_write = settings.savedata_load && Config::Get(Config::NETPLAY_SAVEDATA_WRITE);
|
|
settings.savedata_sync_all_wii =
|
|
settings.savedata_load && Config::Get(Config::NETPLAY_SAVEDATA_SYNC_ALL_WII);
|
|
|
|
settings.strict_settings_sync = Config::Get(Config::NETPLAY_STRICT_SETTINGS_SYNC);
|
|
settings.sync_codes = Config::Get(Config::NETPLAY_SYNC_CODES);
|
|
settings.golf_mode = Config::Get(Config::NETPLAY_NETWORK_MODE) == "golf";
|
|
settings.use_fma = DoAllPlayersHaveHardwareFMA();
|
|
settings.hide_remote_gbas = Config::Get(Config::NETPLAY_HIDE_REMOTE_GBAS);
|
|
|
|
// Unload GameINI to restore things to normal
|
|
Config::RemoveLayer(Config::LayerType::GlobalGame);
|
|
Config::RemoveLayer(Config::LayerType::LocalGame);
|
|
|
|
m_settings = settings;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool NetPlayServer::DoAllPlayersHaveIPLDump() const
|
|
{
|
|
return std::all_of(m_players.begin(), m_players.end(),
|
|
[](const auto& p) { return p.second.has_ipl_dump; });
|
|
}
|
|
|
|
bool NetPlayServer::DoAllPlayersHaveHardwareFMA() const
|
|
{
|
|
return std::all_of(m_players.begin(), m_players.end(),
|
|
[](const auto& p) { return p.second.has_hardware_fma; });
|
|
}
|
|
|
|
struct SaveSyncInfo
|
|
{
|
|
u8 save_count = 0;
|
|
std::shared_ptr<const UICommon::GameFile> game;
|
|
bool has_wii_save = false;
|
|
std::unique_ptr<IOS::HLE::FS::FileSystem> configured_fs;
|
|
std::optional<std::vector<u8>> mii_data;
|
|
std::vector<std::pair<u64, WiiSave::StoragePointer>> wii_saves;
|
|
std::optional<DiscIO::Riivolution::SavegameRedirect> redirected_save;
|
|
};
|
|
|
|
// called from ---GUI--- thread
|
|
bool NetPlayServer::RequestStartGame()
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "Start Game requested.");
|
|
|
|
if (!SetupNetSettings())
|
|
return false;
|
|
|
|
bool start_now = true;
|
|
|
|
if (m_settings.savedata_load)
|
|
{
|
|
auto save_sync_info = CollectSaveSyncInfo();
|
|
if (!save_sync_info)
|
|
{
|
|
PanicAlertFmtT("Error collecting save data!");
|
|
m_start_pending = false;
|
|
return false;
|
|
}
|
|
|
|
if (save_sync_info->has_wii_save)
|
|
{
|
|
// Set titles for host-side loading in WiiRoot
|
|
std::vector<u64> titles;
|
|
for (const auto& [title_id, storage] : save_sync_info->wii_saves)
|
|
titles.push_back(title_id);
|
|
m_dialog->SetHostWiiSyncData(
|
|
std::move(titles),
|
|
save_sync_info->redirected_save ? save_sync_info->redirected_save->m_target_path : "");
|
|
}
|
|
|
|
if (m_players.size() > 1)
|
|
{
|
|
start_now = false;
|
|
m_start_pending = true;
|
|
if (!SyncSaveData(*save_sync_info))
|
|
{
|
|
PanicAlertFmtT("Error synchronizing save data!");
|
|
m_start_pending = false;
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check To Send Codes to Clients
|
|
if (m_settings.sync_codes && m_players.size() > 1)
|
|
{
|
|
start_now = false;
|
|
m_start_pending = true;
|
|
if (!SyncCodes())
|
|
{
|
|
PanicAlertFmtT("Error synchronizing cheat codes!");
|
|
m_start_pending = false;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (start_now)
|
|
{
|
|
return StartGame();
|
|
}
|
|
|
|
INFO_LOG_FMT(NETPLAY, "Waiting for data sync with clients.");
|
|
return true;
|
|
}
|
|
|
|
// called from multiple threads
|
|
bool NetPlayServer::StartGame()
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "Starting game.");
|
|
|
|
m_timebase_by_frame.clear();
|
|
m_desync_detected = false;
|
|
std::lock_guard lkg(m_crit.game);
|
|
// only used as an identifier, not time value, so truncation is fine
|
|
m_current_game = static_cast<u32>(Common::Timer::NowMs());
|
|
|
|
// no change, just update with clients
|
|
if (!m_host_input_authority)
|
|
AdjustPadBufferSize(m_target_buffer_size);
|
|
|
|
m_current_golfer = 1;
|
|
m_pending_golfer = 0;
|
|
|
|
const sf::Uint64 initial_rtc = GetInitialNetPlayRTC();
|
|
|
|
const std::string region = Config::GetDirectoryForRegion(
|
|
Config::ToGameCubeRegion(m_dialog->FindGameFile(m_selected_game_identifier)->GetRegion()));
|
|
|
|
// load host's GC SRAM
|
|
SConfig::GetInstance().m_strSRAM = File::GetUserPath(F_GCSRAM_IDX);
|
|
InitSRAM(&m_settings.sram, SConfig::GetInstance().m_strSRAM);
|
|
|
|
// tell clients to start game
|
|
sf::Packet spac;
|
|
spac << MessageID::StartGame;
|
|
spac << m_current_game;
|
|
spac << m_settings.cpu_thread;
|
|
spac << m_settings.cpu_core;
|
|
spac << m_settings.enable_cheats;
|
|
spac << m_settings.selected_language;
|
|
spac << m_settings.override_region_settings;
|
|
spac << m_settings.dsp_enable_jit;
|
|
spac << m_settings.dsp_hle;
|
|
spac << m_settings.ram_override_enable;
|
|
spac << m_settings.mem1_size;
|
|
spac << m_settings.mem2_size;
|
|
spac << m_settings.fallback_region;
|
|
spac << m_settings.allow_sd_writes;
|
|
spac << m_settings.oc_enable;
|
|
spac << m_settings.oc_factor;
|
|
|
|
for (auto slot : ExpansionInterface::SLOTS)
|
|
spac << static_cast<int>(m_settings.exi_device[slot]);
|
|
|
|
spac << m_settings.memcard_size_override;
|
|
|
|
for (u32 value : m_settings.sysconf_settings)
|
|
spac << value;
|
|
|
|
spac << m_settings.efb_access_enable;
|
|
spac << m_settings.bbox_enable;
|
|
spac << m_settings.force_progressive;
|
|
spac << m_settings.efb_to_texture_enable;
|
|
spac << m_settings.xfb_to_texture_enable;
|
|
spac << m_settings.disable_copy_to_vram;
|
|
spac << m_settings.immediate_xfb_enable;
|
|
spac << m_settings.efb_emulate_format_changes;
|
|
spac << m_settings.safe_texture_cache_color_samples;
|
|
spac << m_settings.perf_queries_enable;
|
|
spac << m_settings.float_exceptions;
|
|
spac << m_settings.divide_by_zero_exceptions;
|
|
spac << m_settings.fprf;
|
|
spac << m_settings.accurate_nans;
|
|
spac << m_settings.disable_icache;
|
|
spac << m_settings.sync_on_skip_idle;
|
|
spac << m_settings.sync_gpu;
|
|
spac << m_settings.sync_gpu_max_distance;
|
|
spac << m_settings.sync_gpu_min_distance;
|
|
spac << m_settings.sync_gpu_overclock;
|
|
spac << m_settings.jit_follow_branch;
|
|
spac << m_settings.fast_disc_speed;
|
|
spac << m_settings.mmu;
|
|
spac << m_settings.fastmem;
|
|
spac << m_settings.skip_ipl;
|
|
spac << m_settings.load_ipl_dump;
|
|
spac << m_settings.vertex_rounding;
|
|
spac << m_settings.internal_resolution;
|
|
spac << m_settings.efb_scaled_copy;
|
|
spac << m_settings.fast_depth_calc;
|
|
spac << m_settings.enable_pixel_lighting;
|
|
spac << m_settings.widescreen_hack;
|
|
spac << m_settings.force_texture_filtering;
|
|
spac << m_settings.max_anisotropy;
|
|
spac << m_settings.force_true_color;
|
|
spac << m_settings.disable_copy_filter;
|
|
spac << m_settings.disable_fog;
|
|
spac << m_settings.arbitrary_mipmap_detection;
|
|
spac << m_settings.arbitrary_mipmap_detection_threshold;
|
|
spac << m_settings.enable_gpu_texture_decoding;
|
|
spac << m_settings.defer_efb_copies;
|
|
spac << m_settings.efb_access_tile_size;
|
|
spac << m_settings.efb_access_defer_invalidation;
|
|
spac << m_settings.savedata_load;
|
|
spac << m_settings.savedata_write;
|
|
spac << m_settings.savedata_sync_all_wii;
|
|
spac << m_settings.strict_settings_sync;
|
|
spac << initial_rtc;
|
|
spac << region;
|
|
spac << m_settings.sync_codes;
|
|
|
|
spac << m_settings.golf_mode;
|
|
spac << m_settings.use_fma;
|
|
spac << m_settings.hide_remote_gbas;
|
|
|
|
for (size_t i = 0; i < sizeof(m_settings.sram); ++i)
|
|
spac << m_settings.sram[i];
|
|
|
|
SendAsyncToClients(std::move(spac));
|
|
|
|
m_start_pending = false;
|
|
m_is_running = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
void NetPlayServer::AbortGameStart()
|
|
{
|
|
if (m_start_pending)
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "Aborting game start.");
|
|
m_dialog->OnGameStartAborted();
|
|
ChunkedDataAbort();
|
|
m_start_pending = false;
|
|
}
|
|
else
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "Aborting game start but no game start pending.");
|
|
}
|
|
}
|
|
|
|
// called from ---GUI--- thread
|
|
std::optional<SaveSyncInfo> NetPlayServer::CollectSaveSyncInfo()
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "Collecting saves.");
|
|
|
|
SaveSyncInfo sync_info;
|
|
|
|
sync_info.save_count = 0;
|
|
for (ExpansionInterface::Slot slot : ExpansionInterface::MEMCARD_SLOTS)
|
|
{
|
|
if (m_settings.exi_device[slot] == ExpansionInterface::EXIDeviceType::MemoryCard)
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "Adding memory card (raw) in slot {}.", slot);
|
|
++sync_info.save_count;
|
|
}
|
|
else if (Config::Get(Config::GetInfoForEXIDevice(slot)) ==
|
|
ExpansionInterface::EXIDeviceType::MemoryCardFolder)
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "Adding memory card (folder) in slot {}.", slot);
|
|
++sync_info.save_count;
|
|
}
|
|
}
|
|
|
|
sync_info.game = m_dialog->FindGameFile(m_selected_game_identifier);
|
|
if (sync_info.game == nullptr)
|
|
{
|
|
PanicAlertFmtT("Selected game doesn't exist in game list!");
|
|
return std::nullopt;
|
|
}
|
|
|
|
sync_info.has_wii_save = false;
|
|
if (m_settings.savedata_load && (sync_info.game->GetPlatform() == DiscIO::Platform::WiiDisc ||
|
|
sync_info.game->GetPlatform() == DiscIO::Platform::WiiWAD ||
|
|
sync_info.game->GetPlatform() == DiscIO::Platform::ELFOrDOL))
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "Adding Wii saves.");
|
|
|
|
sync_info.has_wii_save = true;
|
|
++sync_info.save_count;
|
|
|
|
sync_info.configured_fs = IOS::HLE::FS::MakeFileSystem(IOS::HLE::FS::Location::Configured);
|
|
if (m_settings.savedata_sync_all_wii)
|
|
{
|
|
IOS::HLE::Kernel ios;
|
|
for (const u64 title : ios.GetESCore().GetInstalledTitles())
|
|
{
|
|
auto save = WiiSave::MakeNandStorage(sync_info.configured_fs.get(), title);
|
|
if (save && save->ReadHeader().has_value() && save->ReadBkHeader().has_value() &&
|
|
save->ReadFiles().has_value())
|
|
{
|
|
sync_info.wii_saves.emplace_back(title, std::move(save));
|
|
}
|
|
else
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "Skipping Wii save of title {:016x}.", title);
|
|
}
|
|
}
|
|
}
|
|
else if (sync_info.game->GetPlatform() == DiscIO::Platform::WiiDisc ||
|
|
sync_info.game->GetPlatform() == DiscIO::Platform::WiiWAD)
|
|
{
|
|
auto save =
|
|
WiiSave::MakeNandStorage(sync_info.configured_fs.get(), sync_info.game->GetTitleID());
|
|
sync_info.wii_saves.emplace_back(sync_info.game->GetTitleID(), std::move(save));
|
|
}
|
|
|
|
{
|
|
auto file = sync_info.configured_fs->OpenFile(
|
|
IOS::PID_KERNEL, IOS::PID_KERNEL, Common::GetMiiDatabasePath(), IOS::HLE::FS::Mode::Read);
|
|
if (file)
|
|
{
|
|
std::vector<u8> file_data(file->GetStatus()->size);
|
|
if (!file->Read(file_data.data(), file_data.size()))
|
|
return std::nullopt;
|
|
sync_info.mii_data = std::move(file_data);
|
|
}
|
|
}
|
|
|
|
if (sync_info.game->GetBlobType() == DiscIO::BlobType::MOD_DESCRIPTOR)
|
|
{
|
|
auto boot_params = BootParameters::GenerateFromFile(sync_info.game->GetFilePath());
|
|
if (boot_params)
|
|
{
|
|
sync_info.redirected_save =
|
|
DiscIO::Riivolution::ExtractSavegameRedirect(boot_params->riivolution_patches);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (size_t i = 0; i < m_gba_config.size(); ++i)
|
|
{
|
|
const auto& config = m_gba_config[i];
|
|
if (config.enabled && config.has_rom)
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "Adding GBA save in slot {}.", i);
|
|
++sync_info.save_count;
|
|
}
|
|
}
|
|
|
|
return sync_info;
|
|
}
|
|
|
|
// called from ---GUI--- thread
|
|
bool NetPlayServer::SyncSaveData(const SaveSyncInfo& sync_info)
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "Sending {} savegame chunks to clients.", sync_info.save_count);
|
|
|
|
// We're about to sync saves, so set m_saves_synced to false (waits to start game)
|
|
m_saves_synced = false;
|
|
|
|
m_save_data_synced_players = 0;
|
|
|
|
{
|
|
sf::Packet pac;
|
|
pac << MessageID::SyncSaveData;
|
|
pac << SyncSaveDataID::Notify;
|
|
pac << sync_info.save_count;
|
|
|
|
// send this on the chunked data channel to ensure it's sequenced properly
|
|
SendAsyncToClients(std::move(pac), 0, CHUNKED_DATA_CHANNEL);
|
|
}
|
|
|
|
if (sync_info.save_count == 0)
|
|
return true;
|
|
|
|
const auto game_region = sync_info.game->GetRegion();
|
|
const auto gamecube_region = Config::ToGameCubeRegion(game_region);
|
|
const std::string region = Config::GetDirectoryForRegion(gamecube_region);
|
|
|
|
for (ExpansionInterface::Slot slot : ExpansionInterface::MEMCARD_SLOTS)
|
|
{
|
|
const bool is_slot_a = slot == ExpansionInterface::Slot::A;
|
|
|
|
if (m_settings.exi_device[slot] == ExpansionInterface::EXIDeviceType::MemoryCard)
|
|
{
|
|
const int size_override = m_settings.memcard_size_override;
|
|
const u16 card_size_mbits =
|
|
size_override >= 0 && size_override <= 4 ?
|
|
static_cast<u16>(Memcard::MBIT_SIZE_MEMORY_CARD_59 << size_override) :
|
|
Memcard::MBIT_SIZE_MEMORY_CARD_2043;
|
|
const std::string path = Config::GetMemcardPath(slot, game_region, card_size_mbits);
|
|
|
|
sf::Packet pac;
|
|
pac << MessageID::SyncSaveData;
|
|
pac << SyncSaveDataID::RawData;
|
|
pac << is_slot_a << region << size_override;
|
|
|
|
if (File::Exists(path))
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "Sending data of raw memcard {} in slot {}.", path,
|
|
is_slot_a ? 'A' : 'B');
|
|
if (!CompressFileIntoPacket(path, pac))
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
// No file, so we'll say the size is 0
|
|
INFO_LOG_FMT(NETPLAY, "Sending empty marker for raw memcard {} in slot {}.", path,
|
|
is_slot_a ? 'A' : 'B');
|
|
pac << sf::Uint64{0};
|
|
}
|
|
|
|
SendChunkedToClients(std::move(pac), 1,
|
|
fmt::format("Memory Card {} Synchronization", is_slot_a ? 'A' : 'B'));
|
|
}
|
|
else if (Config::Get(Config::GetInfoForEXIDevice(slot)) ==
|
|
ExpansionInterface::EXIDeviceType::MemoryCardFolder)
|
|
{
|
|
const std::string path = Config::GetGCIFolderPath(slot, gamecube_region);
|
|
|
|
sf::Packet pac;
|
|
pac << MessageID::SyncSaveData;
|
|
pac << SyncSaveDataID::GCIData;
|
|
pac << is_slot_a;
|
|
|
|
if (File::IsDirectory(path))
|
|
{
|
|
std::vector<std::string> files =
|
|
GCMemcardDirectory::GetFileNamesForGameID(path + DIR_SEP, sync_info.game->GetGameID());
|
|
|
|
INFO_LOG_FMT(NETPLAY, "Sending data of GCI memcard {} in slot {} ({} files).", path,
|
|
is_slot_a ? 'A' : 'B', files.size());
|
|
|
|
pac << static_cast<u8>(files.size());
|
|
|
|
for (const std::string& file : files)
|
|
{
|
|
const std::string filename = file.substr(file.find_last_of('/') + 1);
|
|
INFO_LOG_FMT(NETPLAY, "Sending GCI {}.", filename);
|
|
pac << filename;
|
|
if (!CompressFileIntoPacket(file, pac))
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "Sending empty marker for GCI memcard {} in slot {}.", path,
|
|
is_slot_a ? 'A' : 'B');
|
|
|
|
pac << static_cast<u8>(0);
|
|
}
|
|
|
|
SendChunkedToClients(std::move(pac), 1,
|
|
fmt::format("GCI Folder {} Synchronization", is_slot_a ? 'A' : 'B'));
|
|
}
|
|
}
|
|
|
|
if (sync_info.has_wii_save)
|
|
{
|
|
sf::Packet pac;
|
|
pac << MessageID::SyncSaveData;
|
|
pac << SyncSaveDataID::WiiData;
|
|
|
|
// Shove the Mii data into the start the packet
|
|
if (sync_info.mii_data)
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "Sending Mii data.");
|
|
pac << true;
|
|
if (!CompressBufferIntoPacket(*sync_info.mii_data, pac))
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "Not sending Mii data.");
|
|
pac << false; // no mii data
|
|
}
|
|
|
|
// Carry on with the save files
|
|
INFO_LOG_FMT(NETPLAY, "Sending {} Wii saves.", sync_info.wii_saves.size());
|
|
pac << static_cast<u32>(sync_info.wii_saves.size());
|
|
|
|
for (const auto& [title_id, storage] : sync_info.wii_saves)
|
|
{
|
|
pac << sf::Uint64{title_id};
|
|
|
|
if (storage->SaveExists())
|
|
{
|
|
const std::optional<WiiSave::Header> header = storage->ReadHeader();
|
|
const std::optional<WiiSave::BkHeader> bk_header = storage->ReadBkHeader();
|
|
const std::optional<std::vector<WiiSave::Storage::SaveFile>> files = storage->ReadFiles();
|
|
if (!header || !bk_header || !files)
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "Wii save of title {:016x} is corrupted.", title_id);
|
|
return false;
|
|
}
|
|
|
|
INFO_LOG_FMT(NETPLAY, "Sending Wii save of title {:016x}.", title_id);
|
|
pac << true; // save exists
|
|
|
|
// Header
|
|
pac << sf::Uint64{header->tid};
|
|
pac << header->banner_size << header->permissions << header->unk1;
|
|
for (u8 byte : header->md5)
|
|
pac << byte;
|
|
pac << header->unk2;
|
|
for (size_t i = 0; i < header->banner_size; i++)
|
|
pac << header->banner[i];
|
|
|
|
// BkHeader
|
|
pac << bk_header->size << bk_header->magic << bk_header->ngid << bk_header->number_of_files
|
|
<< bk_header->size_of_files << bk_header->unk1 << bk_header->unk2
|
|
<< bk_header->total_size;
|
|
for (u8 byte : bk_header->unk3)
|
|
pac << byte;
|
|
pac << sf::Uint64{bk_header->tid};
|
|
for (u8 byte : bk_header->mac_address)
|
|
pac << byte;
|
|
|
|
// Files
|
|
for (const WiiSave::Storage::SaveFile& file : *files)
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "Sending Wii save data of type {} at {}",
|
|
static_cast<u8>(file.type), file.path);
|
|
|
|
pac << file.mode << file.attributes << file.type << file.path;
|
|
|
|
if (file.type == WiiSave::Storage::SaveFile::Type::File)
|
|
{
|
|
const std::optional<std::vector<u8>>& data = *file.data;
|
|
if (!data || !CompressBufferIntoPacket(*data, pac))
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "No data for Wii save of title {:016x}.", title_id);
|
|
pac << false; // save does not exist
|
|
}
|
|
}
|
|
|
|
if (sync_info.redirected_save)
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "Sending redirected save at {}.",
|
|
sync_info.redirected_save->m_target_path);
|
|
pac << true;
|
|
if (!CompressFolderIntoPacket(sync_info.redirected_save->m_target_path, pac))
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "Not sending redirected save.");
|
|
pac << false; // no redirected save
|
|
}
|
|
|
|
SendChunkedToClients(std::move(pac), 1, "Wii Save Synchronization");
|
|
}
|
|
|
|
for (size_t i = 0; i < m_gba_config.size(); ++i)
|
|
{
|
|
if (m_gba_config[i].enabled && m_gba_config[i].has_rom)
|
|
{
|
|
sf::Packet pac;
|
|
pac << MessageID::SyncSaveData;
|
|
pac << SyncSaveDataID::GBAData;
|
|
pac << static_cast<u8>(i);
|
|
|
|
std::string path;
|
|
#ifdef HAS_LIBMGBA
|
|
path = HW::GBA::Core::GetSavePath(Config::Get(Config::MAIN_GBA_ROM_PATHS[i]),
|
|
static_cast<int>(i));
|
|
#endif
|
|
if (File::Exists(path))
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "Sending data of GBA save at {} for slot {}.", path, i);
|
|
if (!CompressFileIntoPacket(path, pac))
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
// No file, so we'll say the size is 0
|
|
INFO_LOG_FMT(NETPLAY, "Sending empty marker for GBA save at {} for slot {}.", path, i);
|
|
pac << sf::Uint64{0};
|
|
}
|
|
|
|
SendChunkedToClients(std::move(pac), 1,
|
|
fmt::format("GBA{} Save File Synchronization", i + 1));
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool NetPlayServer::SyncCodes()
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "Sending codes to clients.");
|
|
|
|
// Sync Codes is ticked, so set m_codes_synced to false
|
|
m_codes_synced = false;
|
|
|
|
// Get Game Path
|
|
const auto game = m_dialog->FindGameFile(m_selected_game_identifier);
|
|
if (game == nullptr)
|
|
{
|
|
PanicAlertFmtT("Selected game doesn't exist in game list!");
|
|
return false;
|
|
}
|
|
|
|
// Find all INI files
|
|
const auto game_id = game->GetGameID();
|
|
const auto revision = game->GetRevision();
|
|
Common::IniFile globalIni;
|
|
for (const std::string& filename : ConfigLoaders::GetGameIniFilenames(game_id, revision))
|
|
globalIni.Load(File::GetSysDirectory() + GAMESETTINGS_DIR DIR_SEP + filename, true);
|
|
Common::IniFile localIni;
|
|
for (const std::string& filename : ConfigLoaders::GetGameIniFilenames(game_id, revision))
|
|
localIni.Load(File::GetUserPath(D_GAMESETTINGS_IDX) + filename, true);
|
|
|
|
// Initialize Number of Synced Players
|
|
m_codes_synced_players = 0;
|
|
|
|
// Notify Clients of Incoming Code Sync
|
|
{
|
|
sf::Packet pac;
|
|
pac << MessageID::SyncCodes;
|
|
pac << SyncCodeID::Notify;
|
|
SendAsyncToClients(std::move(pac));
|
|
}
|
|
// Sync Gecko Codes
|
|
{
|
|
// Create a Gecko Code Vector with just the active codes
|
|
std::vector<Gecko::GeckoCode> s_active_codes =
|
|
Gecko::SetAndReturnActiveCodes(Gecko::LoadCodes(globalIni, localIni));
|
|
|
|
// Determine Codelist Size
|
|
u16 codelines = 0;
|
|
for (const Gecko::GeckoCode& active_code : s_active_codes)
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "Indexing {}", active_code.name);
|
|
for (const Gecko::GeckoCode::Code& code : active_code.codes)
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "{:08x} {:08x}", code.address, code.data);
|
|
codelines++;
|
|
}
|
|
}
|
|
|
|
// Output codelines to send
|
|
INFO_LOG_FMT(NETPLAY, "Sending {} Gecko codelines", codelines);
|
|
|
|
// Send initial packet. Notify of the sync operation and total number of lines being sent.
|
|
{
|
|
sf::Packet pac;
|
|
pac << MessageID::SyncCodes;
|
|
pac << SyncCodeID::NotifyGecko;
|
|
pac << codelines;
|
|
SendAsyncToClients(std::move(pac));
|
|
}
|
|
|
|
// Send entire codeset in the second packet
|
|
{
|
|
sf::Packet pac;
|
|
pac << MessageID::SyncCodes;
|
|
pac << SyncCodeID::GeckoData;
|
|
// Iterate through the active code vector and send each codeline
|
|
for (const Gecko::GeckoCode& active_code : s_active_codes)
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "Sending {}", active_code.name);
|
|
for (const Gecko::GeckoCode::Code& code : active_code.codes)
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "{:08x} {:08x}", code.address, code.data);
|
|
pac << code.address;
|
|
pac << code.data;
|
|
}
|
|
}
|
|
SendAsyncToClients(std::move(pac));
|
|
}
|
|
}
|
|
|
|
// Sync AR Codes
|
|
{
|
|
// Create an AR Code Vector with just the active codes
|
|
std::vector<ActionReplay::ARCode> s_active_codes =
|
|
ActionReplay::ApplyAndReturnCodes(ActionReplay::LoadCodes(globalIni, localIni));
|
|
|
|
// Determine Codelist Size
|
|
u16 codelines = 0;
|
|
for (const ActionReplay::ARCode& active_code : s_active_codes)
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "Indexing {}", active_code.name);
|
|
for (const ActionReplay::AREntry& op : active_code.ops)
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "{:08x} {:08x}", op.cmd_addr, op.value);
|
|
codelines++;
|
|
}
|
|
}
|
|
|
|
// Output codelines to send
|
|
INFO_LOG_FMT(NETPLAY, "Sending {} AR codelines", codelines);
|
|
|
|
// Send initial packet. Notify of the sync operation and total number of lines being sent.
|
|
{
|
|
sf::Packet pac;
|
|
pac << MessageID::SyncCodes;
|
|
pac << SyncCodeID::NotifyAR;
|
|
pac << codelines;
|
|
SendAsyncToClients(std::move(pac));
|
|
}
|
|
|
|
// Send entire codeset in the second packet
|
|
{
|
|
sf::Packet pac;
|
|
pac << MessageID::SyncCodes;
|
|
pac << SyncCodeID::ARData;
|
|
// Iterate through the active code vector and send each codeline
|
|
for (const ActionReplay::ARCode& active_code : s_active_codes)
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "Sending {}", active_code.name);
|
|
for (const ActionReplay::AREntry& op : active_code.ops)
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "{:08x} {:08x}", op.cmd_addr, op.value);
|
|
pac << op.cmd_addr;
|
|
pac << op.value;
|
|
}
|
|
}
|
|
SendAsyncToClients(std::move(pac));
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void NetPlayServer::CheckSyncAndStartGame()
|
|
{
|
|
if (m_saves_synced && m_codes_synced)
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "Synchronized, starting game.");
|
|
StartGame();
|
|
}
|
|
else
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "Not synchronized.");
|
|
}
|
|
}
|
|
|
|
u64 NetPlayServer::GetInitialNetPlayRTC() const
|
|
{
|
|
if (Config::Get(Config::MAIN_CUSTOM_RTC_ENABLE))
|
|
return Config::Get(Config::MAIN_CUSTOM_RTC_VALUE);
|
|
|
|
return Common::Timer::GetLocalTimeSinceJan1970();
|
|
}
|
|
|
|
// called from multiple threads
|
|
void NetPlayServer::SendToClients(const sf::Packet& packet, const PlayerId skip_pid,
|
|
const u8 channel_id)
|
|
{
|
|
for (auto& p : m_players)
|
|
{
|
|
if (p.second.pid && p.second.pid != skip_pid)
|
|
{
|
|
Send(p.second.socket, packet, channel_id);
|
|
}
|
|
}
|
|
}
|
|
|
|
void NetPlayServer::Send(ENetPeer* socket, const sf::Packet& packet, const u8 channel_id)
|
|
{
|
|
Common::ENet::SendPacket(socket, packet, channel_id);
|
|
}
|
|
|
|
void NetPlayServer::KickPlayer(PlayerId player)
|
|
{
|
|
for (auto& current_player : m_players)
|
|
{
|
|
if (current_player.second.pid == player)
|
|
{
|
|
enet_peer_disconnect(current_player.second.socket, 0);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool NetPlayServer::PlayerHasControllerMapped(const PlayerId pid) const
|
|
{
|
|
const auto mapping_matches_player_id = [pid](const PlayerId& mapping) { return mapping == pid; };
|
|
|
|
return std::any_of(m_pad_map.begin(), m_pad_map.end(), mapping_matches_player_id) ||
|
|
std::any_of(m_wiimote_map.begin(), m_wiimote_map.end(), mapping_matches_player_id);
|
|
}
|
|
|
|
void NetPlayServer::AssignNewUserAPad(const Client& player)
|
|
{
|
|
for (PlayerId& mapping : m_pad_map)
|
|
{
|
|
// 0 means unmapped
|
|
if (mapping == 0)
|
|
{
|
|
mapping = player.pid;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
PlayerId NetPlayServer::GiveFirstAvailableIDTo(ENetPeer* player)
|
|
{
|
|
PlayerId pid = 1;
|
|
for (auto i = m_players.begin(); i != m_players.end(); ++i)
|
|
{
|
|
if (i->second.pid == pid)
|
|
{
|
|
pid++;
|
|
i = m_players.begin();
|
|
}
|
|
}
|
|
player->data = new PlayerId(pid);
|
|
return pid;
|
|
}
|
|
|
|
template <typename... Data>
|
|
void NetPlayServer::SendResponseToPlayer(const Client& player, const MessageID message_id,
|
|
Data&&... data_to_send)
|
|
{
|
|
sf::Packet response;
|
|
response << message_id;
|
|
// this is a C++17 fold expression used to call the << operator for all of the data
|
|
(response << ... << std::forward<Data>(data_to_send));
|
|
|
|
Send(player.socket, response);
|
|
}
|
|
|
|
template <typename... Data>
|
|
void NetPlayServer::SendResponseToAllPlayers(const MessageID message_id, Data&&... data_to_send)
|
|
{
|
|
sf::Packet response;
|
|
response << message_id;
|
|
// this is a C++17 fold expression used to call the << operator for all of the data
|
|
(response << ... << std::forward<Data>(data_to_send));
|
|
|
|
SendToClients(response);
|
|
}
|
|
|
|
u16 NetPlayServer::GetPort() const
|
|
{
|
|
return m_server->address.port;
|
|
}
|
|
|
|
// called from ---GUI--- thread
|
|
std::unordered_set<std::string> NetPlayServer::GetInterfaceSet() const
|
|
{
|
|
std::unordered_set<std::string> result;
|
|
for (const auto& list_entry : GetInterfaceListInternal())
|
|
result.emplace(list_entry.first);
|
|
return result;
|
|
}
|
|
|
|
// called from ---GUI--- thread
|
|
std::string NetPlayServer::GetInterfaceHost(const std::string& inter) const
|
|
{
|
|
char buf[16];
|
|
sprintf(buf, ":%d", GetPort());
|
|
auto lst = GetInterfaceListInternal();
|
|
for (const auto& list_entry : lst)
|
|
{
|
|
if (list_entry.first == inter)
|
|
{
|
|
return list_entry.second + buf;
|
|
}
|
|
}
|
|
return "?";
|
|
}
|
|
|
|
// called from ---GUI--- thread
|
|
std::vector<std::pair<std::string, std::string>> NetPlayServer::GetInterfaceListInternal() const
|
|
{
|
|
std::vector<std::pair<std::string, std::string>> result;
|
|
#if defined(_WIN32)
|
|
|
|
#elif defined(ANDROID)
|
|
// Android has no getifaddrs for some stupid reason. If this
|
|
// functionality ends up actually being used on Android, fix this.
|
|
#else
|
|
ifaddrs* ifp = nullptr;
|
|
char buf[512];
|
|
if (getifaddrs(&ifp) != -1)
|
|
{
|
|
for (ifaddrs* curifp = ifp; curifp; curifp = curifp->ifa_next)
|
|
{
|
|
sockaddr* sa = curifp->ifa_addr;
|
|
|
|
if (sa == nullptr)
|
|
continue;
|
|
if (sa->sa_family != AF_INET)
|
|
continue;
|
|
sockaddr_in* sai = (struct sockaddr_in*)sa;
|
|
if (ntohl(((struct sockaddr_in*)sa)->sin_addr.s_addr) == 0x7f000001)
|
|
continue;
|
|
const char* ip = inet_ntop(sa->sa_family, &sai->sin_addr, buf, sizeof(buf));
|
|
if (ip == nullptr)
|
|
continue;
|
|
result.emplace_back(std::make_pair(curifp->ifa_name, ip));
|
|
}
|
|
freeifaddrs(ifp);
|
|
}
|
|
#endif
|
|
if (result.empty())
|
|
result.emplace_back(std::make_pair("!local!", "127.0.0.1"));
|
|
return result;
|
|
}
|
|
|
|
// called from ---Chunked Data--- thread
|
|
void NetPlayServer::ChunkedDataThreadFunc()
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "Starting Chunked Data Thread.");
|
|
|
|
while (m_do_loop)
|
|
{
|
|
m_chunked_data_event.Wait();
|
|
|
|
if (m_abort_chunked_data)
|
|
{
|
|
// thread-safe clear
|
|
while (!m_chunked_data_queue.Empty())
|
|
m_chunked_data_queue.Pop();
|
|
|
|
m_abort_chunked_data = false;
|
|
}
|
|
|
|
while (!m_chunked_data_queue.Empty())
|
|
{
|
|
if (!m_do_loop)
|
|
return;
|
|
if (m_abort_chunked_data)
|
|
break;
|
|
auto& e = m_chunked_data_queue.Front();
|
|
const u32 id = m_next_chunked_data_id++;
|
|
|
|
m_chunked_data_complete_count[id] = 0;
|
|
size_t player_count;
|
|
{
|
|
std::vector<int> players;
|
|
if (e.target_mode == TargetMode::Only)
|
|
{
|
|
players.push_back(e.target_pid);
|
|
}
|
|
else
|
|
{
|
|
for (auto& pl : m_players)
|
|
{
|
|
if (pl.second.pid != e.target_pid)
|
|
players.push_back(pl.second.pid);
|
|
}
|
|
}
|
|
player_count = players.size();
|
|
|
|
INFO_LOG_FMT(NETPLAY, "Informing players {} of data chunk {} start.",
|
|
fmt::join(players, ", "), id);
|
|
|
|
sf::Packet pac;
|
|
pac << MessageID::ChunkedDataStart;
|
|
pac << id << e.title << sf::Uint64{e.packet.getDataSize()};
|
|
|
|
ChunkedDataSend(std::move(pac), e.target_pid, e.target_mode);
|
|
|
|
if (e.target_mode == TargetMode::AllExcept && e.target_pid == 1)
|
|
m_dialog->ShowChunkedProgressDialog(e.title, e.packet.getDataSize(), players);
|
|
}
|
|
|
|
const bool enable_limit = Config::Get(Config::NETPLAY_ENABLE_CHUNKED_UPLOAD_LIMIT);
|
|
const float bytes_per_second =
|
|
(std::max(Config::Get(Config::NETPLAY_CHUNKED_UPLOAD_LIMIT), 1u) / 8.0f) * 1024.0f;
|
|
const std::chrono::duration<double> send_interval(CHUNKED_DATA_UNIT_SIZE / bytes_per_second);
|
|
bool skip_wait = false;
|
|
size_t index = 0;
|
|
do
|
|
{
|
|
if (!m_do_loop)
|
|
return;
|
|
if (m_abort_chunked_data)
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "Informing players of data chunk {} abort.", id);
|
|
|
|
sf::Packet pac;
|
|
pac << MessageID::ChunkedDataAbort;
|
|
pac << id;
|
|
ChunkedDataSend(std::move(pac), e.target_pid, e.target_mode);
|
|
break;
|
|
}
|
|
if (e.target_mode == TargetMode::Only)
|
|
{
|
|
if (m_players.find(e.target_pid) == m_players.end())
|
|
{
|
|
skip_wait = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
auto start = std::chrono::steady_clock::now();
|
|
|
|
sf::Packet pac;
|
|
pac << MessageID::ChunkedDataPayload;
|
|
pac << id;
|
|
size_t len = std::min(CHUNKED_DATA_UNIT_SIZE, e.packet.getDataSize() - index);
|
|
pac.append(static_cast<const u8*>(e.packet.getData()) + index, len);
|
|
|
|
INFO_LOG_FMT(NETPLAY, "Sending data chunk of {} ({} bytes at {}/{}).", id, len, index,
|
|
e.packet.getDataSize());
|
|
|
|
ChunkedDataSend(std::move(pac), e.target_pid, e.target_mode);
|
|
index += CHUNKED_DATA_UNIT_SIZE;
|
|
|
|
if (enable_limit)
|
|
{
|
|
std::chrono::duration<double> delta = std::chrono::steady_clock::now() - start;
|
|
std::this_thread::sleep_for(send_interval - delta);
|
|
}
|
|
} while (index < e.packet.getDataSize());
|
|
|
|
if (!m_abort_chunked_data)
|
|
{
|
|
INFO_LOG_FMT(NETPLAY, "Informing players of data chunk {} end.", id);
|
|
|
|
sf::Packet pac;
|
|
pac << MessageID::ChunkedDataEnd;
|
|
pac << id;
|
|
ChunkedDataSend(std::move(pac), e.target_pid, e.target_mode);
|
|
}
|
|
|
|
while (m_chunked_data_complete_count[id] < player_count && m_do_loop &&
|
|
!m_abort_chunked_data && !skip_wait)
|
|
m_chunked_data_complete_event.Wait();
|
|
m_chunked_data_complete_count.erase(id);
|
|
m_dialog->HideChunkedProgressDialog();
|
|
|
|
m_chunked_data_queue.Pop();
|
|
}
|
|
}
|
|
|
|
INFO_LOG_FMT(NETPLAY, "Stopping Chunked Data Thread.");
|
|
}
|
|
|
|
// called from ---Chunked Data--- thread
|
|
void NetPlayServer::ChunkedDataSend(sf::Packet&& packet, const PlayerId pid,
|
|
const TargetMode target_mode)
|
|
{
|
|
if (target_mode == TargetMode::Only)
|
|
{
|
|
SendAsync(std::move(packet), pid, CHUNKED_DATA_CHANNEL);
|
|
}
|
|
else
|
|
{
|
|
SendAsyncToClients(std::move(packet), pid, CHUNKED_DATA_CHANNEL);
|
|
}
|
|
}
|
|
|
|
void NetPlayServer::ChunkedDataAbort()
|
|
{
|
|
m_abort_chunked_data = true;
|
|
m_chunked_data_event.Set();
|
|
m_chunked_data_complete_event.Set();
|
|
}
|
|
} // namespace NetPlay
|