Files
MesenCE/Core/Netplay/GameClientConnection.cpp
NovaSquirrelandSour 325c78fcbd GB: Link cable support with two local consoles (#77)
* GB: Link cable support with two local consoles

* GB: Fix crash when color blending is enabled

* GB: Fix formatting

* GB: Link UI fixes, control fixes, audio fixes

Copied from Sour's branch. Also a separate change to use separate savefiles.

* GB: Try to avoid crashes from buffer overflows

* GB: Separate savefiles with linked multiplayer

* GB: Add support for faster link speed

* GB: Move enum to correct file

* GB: Refactor, move serial transfer to function

* -Fixed secondary console sometimes running far ahead of the main console when frame counters did not match (e.g because one console had their LCD disabled longer, etc.) This would cause buffer overflows in the audio buffer.
-Fixed screen tearing when both consoles are not perfectly in sync (because of the LCD being disabled for a different amount of time, etc.)
-Fixed main console not calling PlayQueuedAudio properly mid-frame
-Reuse VS Dualsystem code to merge audio/video when possible/simple and moved that to AvMergeUtilities

Some visual glitches can appear on the secondary screen (for GB) while rewinding, but this can't really be fixed.

* Fixed warning (unused local variable)

* Prevent loading single-console save states when in dual-console modes

* Disable debugger for VS DualSystem's subconsole. Fixes issues when debugging VS DualSystem games

* Code cleanup

* Rework solution to disable debugger on second console to improve performance
Added InternalRunFrame for nes & gb to avoid checking for the presence of the second console after each cpu instruction

* Don't enable the linked GB option by default

* Removed unused function

* Netplay: Force game to reload after updating emulation settings when connecting to the server (or when the host changes game)
This ensures that e.g the GB link cable setting is applied and that the game loads correctly, regardless of the client's link cable settings (otherwise the game could load without the link mode enabled, etc.)

---------

Co-authored-by: Sour <[email protected]>
2026-05-15 02:35:27 -07:00

273 lines
7.2 KiB
C++

#include "pch.h"
#include "Netplay/GameClientConnection.h"
#include "Netplay/HandShakeMessage.h"
#include "Netplay/InputDataMessage.h"
#include "Netplay/MovieDataMessage.h"
#include "Netplay/GameInformationMessage.h"
#include "Netplay/SaveStateMessage.h"
#include "Netplay/ClientConnectionData.h"
#include "Netplay/SelectControllerMessage.h"
#include "Netplay/PlayerListMessage.h"
#include "Netplay/ForceDisconnectMessage.h"
#include "Netplay/ServerInformationMessage.h"
#include "Netplay/GameServer.h"
#include "Shared/BaseControlManager.h"
#include "Shared/Emulator.h"
#include "Shared/EmuSettings.h"
#include "Shared/NotificationManager.h"
#include "Shared/RomFinder.h"
GameClientConnection::GameClientConnection(Emulator* emu, unique_ptr<Socket> socket, ClientConnectionData& connectionData) : GameConnection(emu, std::move(socket))
{
_connectionData = connectionData;
_shutdown = false;
_enableControllers = false;
_minimumQueueSize = 3;
_controllerType = ControllerType::None;
MessageManager::DisplayMessage("NetPlay", "ConnectedToServer");
}
GameClientConnection::~GameClientConnection()
{
Shutdown();
}
void GameClientConnection::Shutdown()
{
if(!_shutdown) {
_shutdown = true;
DisableControllers();
_emu->UnregisterInputProvider(this);
_emu->GetNotificationManager()->SendNotification(ConsoleNotificationType::NetplayStopped);
MessageManager::DisplayMessage("NetPlay", "ConnectionLost");
_emu->GetSettings()->ClearFlag(EmulationFlags::MaximumSpeed);
}
Disconnect();
}
void GameClientConnection::SendHandshake()
{
HandShakeMessage message(HandShakeMessage::GetPasswordHash(_connectionData.Password, _serverSalt), _connectionData.Spectator, _emu->GetSettings()->GetVersion());
SendNetMessage(message);
}
void GameClientConnection::SendControllerSelection(NetplayControllerInfo controller)
{
SelectControllerMessage message(controller);
SendNetMessage(message);
}
void GameClientConnection::ClearInputData()
{
LockHandler lock = _writeLock.AcquireSafe();
for(int i = 0; i < BaseControlDevice::PortCount; i++) {
_inputSize[i] = 0;
_inputData[i].clear();
}
}
void GameClientConnection::ProcessMessage(NetMessage* message)
{
GameInformationMessage* gameInfo;
switch(message->GetType()) {
case MessageType::ServerInformation:
_serverSalt = ((ServerInformationMessage*)message)->GetHashSalt();
SendHandshake();
break;
case MessageType::SaveState:
if(_gameLoaded) {
DisableControllers();
auto lock = _emu->AcquireLock();
ClearInputData();
((SaveStateMessage*)message)->LoadState(_emu);
_enableControllers = true;
InitControlDevice();
}
break;
case MessageType::MovieData:
if(_gameLoaded) {
PushControllerState(((MovieDataMessage*)message)->GetPortNumber(), ((MovieDataMessage*)message)->GetInputState());
}
break;
case MessageType::ForceDisconnect:
MessageManager::DisplayMessage("NetPlay", ((ForceDisconnectMessage*)message)->GetMessage());
break;
case MessageType::PlayerList:
_playerList = ((PlayerListMessage*)message)->GetPlayerList();
break;
case MessageType::GameInformation:
DisableControllers();
{
auto lock = _emu->AcquireLock();
gameInfo = (GameInformationMessage*)message;
if(gameInfo->GetPort().Port != _controllerPort.Port || gameInfo->GetPort().SubPort != _controllerPort.SubPort) {
_controllerPort = gameInfo->GetPort();
}
ClearInputData();
}
_gameLoaded = AttemptLoadGame(gameInfo->GetRomFilename(), gameInfo->GetCrc32());
if(!_gameLoaded) {
_emu->Stop(true);
} else {
_emu->UnregisterInputProvider(this);
_emu->RegisterInputProvider(this);
if(gameInfo->IsPaused()) {
_emu->Pause();
} else {
_emu->Resume();
}
}
break;
default:
break;
}
}
bool GameClientConnection::AttemptLoadGame(string filename, uint32_t crc32)
{
if(filename.size() > 0) {
if(!RomFinder::LoadMatchingRom(_emu, filename, crc32)) {
MessageManager::DisplayMessage("NetPlay", "CouldNotFindRom", filename);
return false;
} else {
return true;
}
}
return false;
}
void GameClientConnection::PushControllerState(uint8_t port, ControlDeviceState state)
{
LockHandler lock = _writeLock.AcquireSafe();
_inputData[port].push_back(state);
_inputSize[port]++;
if(_inputData[port].size() >= _minimumQueueSize) {
_waitForInput[port].Signal();
}
}
void GameClientConnection::DisableControllers()
{
//Used to prevent deadlocks when client is trying to fill its buffer while the host changes the current game/settings/etc. (i.e situations where we need to call Console::Pause())
_enableControllers = false;
ClearInputData();
for(int i = 0; i < BaseControlDevice::PortCount; i++) {
_waitForInput[i].Signal();
}
}
bool GameClientConnection::SetInput(BaseControlDevice* device)
{
if(_enableControllers) {
uint8_t port = device->GetPort();
while(_inputSize[port] == 0) {
_waitForInput[port].Wait();
if(port == 0 && _minimumQueueSize < 10) {
//Increase buffer size - reduces freezes at the cost of additional lag
_minimumQueueSize++;
}
if(_shutdown || !_enableControllers) {
return true;
}
}
LockHandler lock = _writeLock.AcquireSafe();
if(_shutdown || !_enableControllers || _inputSize[port] == 0) {
return true;
}
ControlDeviceState state = _inputData[port].front();
_inputData[port].pop_front();
_inputSize[port]--;
if(_inputData[port].size() > _minimumQueueSize) {
//Too much data, catch up
_emu->GetSettings()->SetFlag(EmulationFlags::MaximumSpeed);
} else {
_emu->GetSettings()->ClearFlag(EmulationFlags::MaximumSpeed);
}
device->SetRawState(state);
}
return true;
}
void GameClientConnection::InitControlDevice()
{
shared_ptr<IConsole> console = _emu->GetConsole();
if(!console) {
return;
}
BaseControlManager* controlManager = console->GetControlManager();
shared_ptr<BaseControlDevice> device = controlManager->GetControlDevice(_controllerPort.Port, _controllerPort.SubPort);
if(device) {
_controllerType = device->GetControllerType();
} else {
_controllerType = ControllerType::None;
}
}
void GameClientConnection::ProcessNotification(ConsoleNotificationType type, void* parameter)
{
if(type == ConsoleNotificationType::GameLoaded) {
_emu->RegisterInputProvider(this);
}
}
void GameClientConnection::SendInput()
{
if(_gameLoaded) {
if(!_controlDevice || _controllerType != _controlDevice->GetControllerType()) {
//Pretend we are using port 0 (to use player 1's keybindings during netplay)
shared_ptr<IConsole> console = _emu->GetConsole();
if(!console) {
return;
}
_controlDevice = console->GetControlManager()->CreateControllerDevice(_controllerType, 0);
}
ControlDeviceState inputState;
if(_controlDevice) {
_controlDevice->SetStateFromInput();
inputState = _controlDevice->GetRawState();
}
if(_lastInputSent != inputState) {
InputDataMessage message(inputState);
SendNetMessage(message);
_lastInputSent = inputState;
}
}
}
void GameClientConnection::SelectController(NetplayControllerInfo controller)
{
SendControllerSelection(controller);
}
vector<NetplayControllerUsageInfo> GameClientConnection::GetControllerList()
{
return GameServer::GetControllerList(_emu, _playerList);
}
NetplayControllerInfo GameClientConnection::GetControllerPort()
{
return _controllerPort;
}