Added NetPlay server force resync and drop player UI functionality.

This commit is contained in:
harry 2024-03-03 13:09:28 -05:00
parent 75e9627aa8
commit ee814f99e5
3 changed files with 255 additions and 33 deletions

View File

@ -237,8 +237,9 @@ void NetPlayServer::processPendingConnections(void)
netPlayAuthReq msg; netPlayAuthReq msg;
msg.toNetworkByteOrder(); sendMsg( client, &msg, sizeof(msg), [&msg]{ msg.toNetworkByteOrder(); } );
sendMsg( client, &msg, sizeof(msg) );
emit clientConnected();
} }
} }
@ -258,17 +259,22 @@ int NetPlayServer::closeAllConnections(void)
delete client; delete client;
} }
clientList.clear(); clientList.clear();
emit clientDisconnected();
roleMask = 0;
return 0; return 0;
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
int NetPlayServer::sendMsg( NetPlayClient *client, void *msg, size_t msgSize) int NetPlayServer::sendMsg( NetPlayClient *client, void *msg, size_t msgSize, std::function<void(void)> netByteOrderConvertFunc)
{ {
QTcpSocket* sock; QTcpSocket* sock;
sock = client->getSocket(); sock = client->getSocket();
netByteOrderConvertFunc();
sock->write( static_cast<const char *>(msg), msgSize ); sock->write( static_cast<const char *>(msg), msgSize );
return 0; return 0;
@ -323,8 +329,7 @@ int NetPlayServer::sendRomLoadReq( NetPlayClient *client )
printf("Sending ROM Load Request: %s %lu\n", filepath, fileSize ); printf("Sending ROM Load Request: %s %lu\n", filepath, fileSize );
FCEUI_SetEmulationPaused(EMULATIONPAUSED_PAUSED); FCEUI_SetEmulationPaused(EMULATIONPAUSED_PAUSED);
msg.toNetworkByteOrder(); sendMsg( client, &msg, sizeof(netPlayLoadRomReq), [&msg]{ msg.toNetworkByteOrder(); } );
sendMsg( client, &msg, sizeof(netPlayLoadRomReq) );
while ( (bytesRead = fread( buf, 1, sizeof(buf), fp )) > 0 ) while ( (bytesRead = fread( buf, 1, sizeof(buf), fp )) > 0 )
{ {
@ -353,8 +358,7 @@ int NetPlayServer::sendStateSyncReq( NetPlayClient *client )
printf("Sending ROM Sync Request\n"); printf("Sending ROM Sync Request\n");
FCEUI_SetEmulationPaused(EMULATIONPAUSED_PAUSED); FCEUI_SetEmulationPaused(EMULATIONPAUSED_PAUSED);
hdr.toNetworkByteOrder(); sendMsg( client, &hdr, sizeof(netPlayMsgHdr), [&hdr]{ hdr.toNetworkByteOrder(); } );
sendMsg( client, &hdr, sizeof(netPlayMsgHdr) );
sendMsg( client, em.buf(), em.size() ); sendMsg( client, em.buf(), em.size() );
opsCrc32 = 0; opsCrc32 = 0;
@ -397,6 +401,18 @@ bool NetPlayServer::claimRole(NetPlayClient* client, int _role)
return success; return success;
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void NetPlayServer::releaseRole(NetPlayClient* client)
{
int role = client->role;
if ( (role >= NETPLAY_PLAYER1) && (role <= NETPLAY_PLAYER4) )
{
int mask = (0x01 << role);
roleMask = roleMask & ~mask;
}
}
//-----------------------------------------------------------------------------
void NetPlayServer::onRomLoad() void NetPlayServer::onRomLoad()
{ {
//printf("New ROM Loaded!\n"); //printf("New ROM Loaded!\n");
@ -420,6 +436,20 @@ void NetPlayServer::onNesReset()
} }
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void NetPlayServer::resyncClient( NetPlayClient *client )
{
sendRomLoadReq( client );
sendStateSyncReq( client );
}
//-----------------------------------------------------------------------------
void NetPlayServer::resyncAllClients()
{
for (auto& client : clientList )
{
resyncClient( client );
}
}
//-----------------------------------------------------------------------------
static void serverMessageCallback( void *userData, void *msgBuf, size_t msgSize ) static void serverMessageCallback( void *userData, void *msgBuf, size_t msgSize )
{ {
NetPlayServer *server = NetPlayServer::GetInstance(); NetPlayServer *server = NetPlayServer::GetInstance();
@ -457,8 +487,7 @@ void NetPlayServer::serverProcessMessage( NetPlayClient *client, void *msgBuf, s
netPlayErrorMsg<128> errorMsg; netPlayErrorMsg<128> errorMsg;
errorMsg.setDisconnectFlag(); errorMsg.setDisconnectFlag();
errorMsg.printf("Invalid Password"); errorMsg.printf("Invalid Password");
errorMsg.toNetworkByteOrder(); sendMsg( client, &errorMsg, errorMsg.hdr.msgSize, [&errorMsg]{ errorMsg.toNetworkByteOrder(); } );
sendMsg( client, &errorMsg, errorMsg.hdr.msgSize );
client->flushData(); client->flushData();
} }
} }
@ -478,8 +507,7 @@ void NetPlayServer::serverProcessMessage( NetPlayClient *client, void *msgBuf, s
netPlayErrorMsg<128> errorMsg; netPlayErrorMsg<128> errorMsg;
errorMsg.setDisconnectFlag(); errorMsg.setDisconnectFlag();
errorMsg.printf("Player %i role is not available", msg->playerId+1); errorMsg.printf("Player %i role is not available", msg->playerId+1);
errorMsg.toNetworkByteOrder(); sendMsg( client, &errorMsg, errorMsg.hdr.msgSize, [&errorMsg]{ errorMsg.toNetworkByteOrder(); } );
sendMsg( client, &errorMsg, errorMsg.hdr.msgSize );
client->flushData(); client->flushData();
} }
} }
@ -496,7 +524,8 @@ void NetPlayServer::serverProcessMessage( NetPlayClient *client, void *msgBuf, s
client->gpData[2] = msg->ctrlState[2]; client->gpData[2] = msg->ctrlState[2];
client->gpData[3] = msg->ctrlState[3]; client->gpData[3] = msg->ctrlState[3];
client->setPaused( (msg->flags & netPlayClientState::PAUSE_FLAG) ? true : false ); client->setPaused( (msg->flags & netPlayClientState::PAUSE_FLAG ) ? true : false );
client->setDesync( (msg->flags & netPlayClientState::DESYNC_FLAG) ? true : false );
NetPlayFrameData data; NetPlayFrameData data;
if ( (msg->opsFrame == 0) || netPlayFrameData.find( msg->opsFrame, data ) ) if ( (msg->opsFrame == 0) || netPlayFrameData.find( msg->opsFrame, data ) )
@ -619,7 +648,6 @@ void NetPlayServer::update(void)
} }
} }
if (client->shouldDestroy()) if (client->shouldDestroy())
{ {
it = clientList.erase(it); it = clientList.erase(it);
@ -630,7 +658,10 @@ void NetPlayServer::update(void)
clientPlayer[i] = nullptr; clientPlayer[i] = nullptr;
} }
} }
releaseRole(client);
delete client; delete client;
//printf("Emit clientDisconnected!\n");
emit clientDisconnected();
} }
else else
{ {
@ -873,12 +904,12 @@ void NetPlayClient::onDisconnect(void)
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void NetPlayClient::onSocketError(QAbstractSocket::SocketError error) void NetPlayClient::onSocketError(QAbstractSocket::SocketError error)
{ {
FCEU_DispMessage("Socket Error",0); //FCEU_DispMessage("Socket Error",0);
QString errorMsg = sock->errorString(); QString errorMsg = sock->errorString();
printf("Error: %s\n", errorMsg.toLocal8Bit().constData()); printf("Error: %s\n", errorMsg.toLocal8Bit().constData());
FCEU_DispMessage("%s", 0, errorMsg.toLocal8Bit().constData()); FCEU_DispMessage("Socket Error: %s", 0, errorMsg.toLocal8Bit().constData());
emit errorOccurred(errorMsg); emit errorOccurred(errorMsg);
} }
@ -932,6 +963,10 @@ void NetPlayClient::update(void)
{ {
statusMsg.flags |= netPlayClientState::PAUSE_FLAG; statusMsg.flags |= netPlayClientState::PAUSE_FLAG;
} }
if (desyncCount > 0)
{
statusMsg.flags |= netPlayClientState::DESYNC_FLAG;
}
statusMsg.frameRdy = inputFrameBack(); statusMsg.frameRdy = inputFrameBack();
statusMsg.frameRun = currFrame; statusMsg.frameRun = currFrame;
statusMsg.opsFrame = lastFrameData.frameNum; statusMsg.opsFrame = lastFrameData.frameNum;
@ -1452,35 +1487,58 @@ NetPlayHostStatusDialog* NetPlayHostStatusDialog::instance = nullptr;
NetPlayHostStatusDialog::NetPlayHostStatusDialog(QWidget *parent) NetPlayHostStatusDialog::NetPlayHostStatusDialog(QWidget *parent)
: QDialog(parent) : QDialog(parent)
{ {
QVBoxLayout *mainLayout; QVBoxLayout *mainLayout, *vbox1;
QHBoxLayout *hbox; QHBoxLayout *hbox;
//QGridLayout *grid; //QGridLayout *grid;
QGroupBox *gbox;
QPushButton *closeButton; QPushButton *closeButton;
//QLabel *lbl; //QLabel *lbl;
instance = this; instance = this;
setWindowTitle("NetPlay Status"); setWindowTitle("NetPlay Status");
resize( 512, 256 );
mainLayout = new QVBoxLayout(); mainLayout = new QVBoxLayout();
vbox1 = new QVBoxLayout();
gbox = new QGroupBox(tr("Connected Players / Spectators"));
clientTree = new QTreeWidget(); clientTree = new QTreeWidget();
mainLayout->addWidget( clientTree ); dropPlayerButton = new QPushButton( tr("Drop Player") );
resyncPlayerButton = new QPushButton( tr("Resync Player") );
resyncAllButton = new QPushButton( tr("Resync All") );
dropPlayerButton->setEnabled(false);
resyncPlayerButton->setEnabled(false);
gbox->setLayout(vbox1);
vbox1->addWidget( clientTree );
hbox = new QHBoxLayout();
hbox->addWidget( dropPlayerButton, 1 );
hbox->addStretch(3);
hbox->addWidget( resyncPlayerButton, 1 );
hbox->addWidget( resyncAllButton, 1 );
vbox1->addLayout(hbox);
mainLayout->addWidget( gbox );
clientTree->setColumnCount(3); clientTree->setColumnCount(3);
auto* item = new QTreeWidgetItem(); auto* item = new QTreeWidgetItem();
item->setText( 0, QString::fromStdString( "Player" ) ); item->setText( 0, QString::fromStdString( "Player" ) );
item->setText( 1, QString::fromStdString( "Role" ) ); item->setText( 1, QString::fromStdString( "Role" ) );
item->setText( 2, QString::fromStdString( "State" ) ); item->setText( 2, QString::fromStdString( "Frame" ) );
item->setText( 3, QString::fromStdString( "State" ) );
item->setTextAlignment( 0, Qt::AlignLeft); item->setTextAlignment( 0, Qt::AlignLeft);
item->setTextAlignment( 1, Qt::AlignLeft); item->setTextAlignment( 1, Qt::AlignLeft);
item->setTextAlignment( 2, Qt::AlignLeft); item->setTextAlignment( 2, Qt::AlignLeft);
item->setTextAlignment( 3, Qt::AlignLeft);
//connect( clientTree, SIGNAL(itemClicked(QTreeWidgetItem*, int)), connect( clientTree, SIGNAL(itemClicked(QTreeWidgetItem*, int)), this, SLOT(clientItemClicked( QTreeWidgetItem*, int)) );
//this, SLOT(watchClicked( QTreeWidgetItem*, int)) );
clientTree->setHeaderItem( item ); clientTree->setHeaderItem( item );
clientTree->setContextMenuPolicy(Qt::CustomContextMenu);
closeButton = new QPushButton( tr("Close") ); closeButton = new QPushButton( tr("Close") );
closeButton->setIcon(style()->standardIcon(QStyle::SP_DialogCloseButton)); closeButton->setIcon(style()->standardIcon(QStyle::SP_DialogCloseButton));
@ -1493,12 +1551,26 @@ NetPlayHostStatusDialog::NetPlayHostStatusDialog(QWidget *parent)
setLayout(mainLayout); setLayout(mainLayout);
connect( NetPlayServer::GetInstance(), SIGNAL(clientConnected(void)), this, SLOT(loadClientTree(void)) );
connect( NetPlayServer::GetInstance(), SIGNAL(clientDisconnected(void)), this, SLOT(loadClientTree(void)) );
connect( clientTree, &QTreeWidget::customContextMenuRequested, this, &NetPlayHostStatusDialog::onClientTreeContextMenu);
connect( dropPlayerButton , SIGNAL(clicked(void)), this, SLOT(dropPlayer(void)) );
connect( resyncPlayerButton, SIGNAL(clicked(void)), this, SLOT(resyncPlayer(void)) );
connect( resyncAllButton , SIGNAL(clicked(void)), this, SLOT(resyncAllPlayers(void)) );
loadClientTree(); loadClientTree();
periodicTimer = new QTimer(this);
periodicTimer->start(200); // 5hz
connect(periodicTimer, &QTimer::timeout, this, &NetPlayHostStatusDialog::updatePeriodic);
} }
//---------------------------------------------------------------------------- //----------------------------------------------------------------------------
NetPlayHostStatusDialog::~NetPlayHostStatusDialog(void) NetPlayHostStatusDialog::~NetPlayHostStatusDialog(void)
{ {
instance = nullptr; instance = nullptr;
periodicTimer->stop();
delete periodicTimer;
//printf("Destroy NetPlay Status Window\n"); //printf("Destroy NetPlay Status Window\n");
} }
//---------------------------------------------------------------------------- //----------------------------------------------------------------------------
@ -1517,20 +1589,142 @@ void NetPlayHostStatusDialog::closeWindow(void)
deleteLater(); deleteLater();
} }
//---------------------------------------------------------------------------- //----------------------------------------------------------------------------
void NetPlayHostStatusDialog::dropPlayer(void)
{
auto* clientItem = static_cast<NetPlayClientTreeItem*>(clientTree->currentItem());
if (clientItem != nullptr)
{
clientItem->client->forceDisconnect();
}
}
//----------------------------------------------------------------------------
void NetPlayHostStatusDialog::resyncPlayer(void)
{
auto* clientItem = static_cast<NetPlayClientTreeItem*>(clientTree->currentItem());
if (clientItem != nullptr)
{
auto* server = NetPlayServer::GetInstance();
if ( (server != nullptr) && (clientItem->client != nullptr) )
{
server->resyncClient( clientItem->client );
}
}
}
//----------------------------------------------------------------------------
void NetPlayHostStatusDialog::resyncAllPlayers(void)
{
auto* server = NetPlayServer::GetInstance();
if (server != nullptr)
{
server->resyncAllClients();
}
}
//----------------------------------------------------------------------------
void NetPlayHostStatusDialog::clientItemClicked( QTreeWidgetItem* item, int column)
{
auto* clientItem = static_cast<NetPlayClientTreeItem*>(item);
bool hasClient = clientItem->client != nullptr;
dropPlayerButton->setEnabled( hasClient );
resyncPlayerButton->setEnabled( hasClient );
}
//----------------------------------------------------------------------------
void NetPlayHostStatusDialog::onClientTreeContextMenu(const QPoint &pos)
{
QAction *act;
QMenu menu(this);
printf("Custom Menu (%i,%i)\n", pos.x(), pos.y());
act = new QAction(tr("Resync Client"), &menu);
//act->setShortcut( QKeySequence(tr("R")));
//connect( act, SIGNAL(triggered(void)), this, SLOT(openTileEditor(void)) );
menu.addAction( act );
act = new QAction(tr("Drop Client"), &menu);
//act->setShortcut( QKeySequence(tr("D")));
//connect( act, SIGNAL(triggered(void)), this, SLOT(openTileEditor(void)) );
menu.addAction( act );
menu.exec( clientTree->mapToGlobal(pos) );
}
//----------------------------------------------------------------------------
void NetPlayClientTreeItem::updateData()
{
const char *roleString = nullptr;
if (server != nullptr)
{
QString state;
roleString = NetPlayPlayerRoleToString( server->getRole() );
if (FCEUI_EmulationPaused())
{
state += QObject::tr("Paused");
}
setText( 0, QObject::tr("Host") );
setText( 1, QObject::tr(roleString) );
setText( 2, QString::number(static_cast<uint32_t>(currFrameCounter)));
setText( 3, state);
}
else if (client != nullptr)
{
QString state;
roleString = NetPlayPlayerRoleToString( client->role );
if (client->isPaused())
{
state += QObject::tr("Paused");
}
if (client->hasDesync())
{
if (!state.isEmpty())
{
state += QObject::tr(",");
}
state += QObject::tr("Desync");
}
setText( 0, client->userName );
setText( 1, QObject::tr(roleString) );
setText( 2, QString::number(client->currentFrame) );
setText( 3, state);
}
}
//----------------------------------------------------------------------------
void NetPlayHostStatusDialog::updatePeriodic()
{
for (int i=0; i<clientTree->topLevelItemCount(); i++)
{
NetPlayClientTreeItem *item = static_cast<NetPlayClientTreeItem*>( clientTree->topLevelItem(i) );
item->updateData();
}
}
//----------------------------------------------------------------------------
void NetPlayHostStatusDialog::loadClientTree() void NetPlayHostStatusDialog::loadClientTree()
{ {
auto* server = NetPlayServer::GetInstance(); auto* server = NetPlayServer::GetInstance();
//printf("Load Client Connection Tree\n");
clientTree->clear();
if (server == nullptr) if (server == nullptr)
{ {
return; return;
} }
const char *roleString = NetPlayPlayerRoleToString( server->getRole() );
auto* serverTopLvlItem = new NetPlayClientTreeItem(); auto* serverTopLvlItem = new NetPlayClientTreeItem();
serverTopLvlItem->server = server; serverTopLvlItem->server = server;
serverTopLvlItem->setText( 0, tr("Host") ); serverTopLvlItem->updateData();
serverTopLvlItem->setText( 1, tr(roleString) );
clientTree->addTopLevelItem( serverTopLvlItem ); clientTree->addTopLevelItem( serverTopLvlItem );
@ -1538,14 +1732,10 @@ void NetPlayHostStatusDialog::loadClientTree()
for (auto& client : clientList) for (auto& client : clientList)
{ {
roleString = NetPlayPlayerRoleToString( client->role );
auto* clientTopLvlItem = new NetPlayClientTreeItem(); auto* clientTopLvlItem = new NetPlayClientTreeItem();
clientTopLvlItem->client = client; clientTopLvlItem->client = client;
clientTopLvlItem->updateData();
clientTopLvlItem->setText( 0, client->userName );
clientTopLvlItem->setText( 1, tr(roleString) );
clientTree->addTopLevelItem( clientTopLvlItem ); clientTree->addTopLevelItem( clientTopLvlItem );
} }
@ -1571,6 +1761,12 @@ void NetPlayPeriodicUpdate(void)
if (client) if (client)
{ {
client->update(); client->update();
if ( client->shouldDestroy() )
{
NetPlayClient::Destroy();
client = nullptr;
}
} }
NetPlayServer *server = NetPlayServer::GetInstance(); NetPlayServer *server = NetPlayServer::GetInstance();

View File

@ -7,6 +7,7 @@
#include <stdlib.h> #include <stdlib.h>
#include <stdint.h> #include <stdint.h>
#include <list> #include <list>
#include <functional>
#include <QWidget> #include <QWidget>
#include <QDialog> #include <QDialog>
@ -124,12 +125,16 @@ class NetPlayServer : public QTcpServer
input.clear(); input.clear();
} }
int sendMsg( NetPlayClient *client, void *msg, size_t msgSize); void resyncClient( NetPlayClient *client );
void resyncAllClients();
int sendMsg( NetPlayClient *client, void *msg, size_t msgSize, std::function<void(void)> netByteOrderConvertFunc = []{});
int sendRomLoadReq( NetPlayClient *client ); int sendRomLoadReq( NetPlayClient *client );
int sendStateSyncReq( NetPlayClient *client ); int sendStateSyncReq( NetPlayClient *client );
void setRole(int _role); void setRole(int _role);
int getRole(void){ return role; } int getRole(void){ return role; }
bool claimRole(NetPlayClient* client, int _role); bool claimRole(NetPlayClient* client, int _role);
void releaseRole(NetPlayClient* client);
uint32_t getMaxLeadFrames(){ return maxLeadFrames; } uint32_t getMaxLeadFrames(){ return maxLeadFrames; }
void setMaxLeadFrames(uint32_t value){ maxLeadFrames = value; } void setMaxLeadFrames(uint32_t value){ maxLeadFrames = value; }
@ -155,6 +160,11 @@ class NetPlayServer : public QTcpServer
uint32_t cycleCounter = 0; uint32_t cycleCounter = 0;
uint32_t maxLeadFrames = 10u; uint32_t maxLeadFrames = 10u;
public:
signals:
void clientConnected(void);
void clientDisconnected(void);
public slots: public slots:
void newConnectionRdy(void); void newConnectionRdy(void);
void onRomLoad(void); void onRomLoad(void);
@ -236,6 +246,8 @@ class NetPlayClient : public QObject
bool shouldDestroy(){ return needsDestroy; } bool shouldDestroy(){ return needsDestroy; }
bool isPaused(){ return paused; } bool isPaused(){ return paused; }
void setPaused(bool value){ paused = value; } void setPaused(bool value){ paused = value; }
bool hasDesync(){ return desync; }
void setDesync(bool value){ desync = value; }
void recordPingResult( uint64_t delay_ms ); void recordPingResult( uint64_t delay_ms );
void resetPingData(void); void resetPingData(void);
double getAvgPingDelay(); double getAvgPingDelay();
@ -263,6 +275,7 @@ class NetPlayClient : public QObject
bool needsDestroy = false; bool needsDestroy = false;
bool _connected = false; bool _connected = false;
bool paused = false; bool paused = false;
bool desync = false;
uint64_t pingDelaySum = 0; uint64_t pingDelaySum = 0;
uint64_t pingDelayLast = 0; uint64_t pingDelayLast = 0;
@ -349,6 +362,8 @@ class NetPlayClientTreeItem : public QTreeWidgetItem
NetPlayClient* client = nullptr; NetPlayClient* client = nullptr;
NetPlayServer* server = nullptr; NetPlayServer* server = nullptr;
void updateData();
private: private:
}; };
@ -364,14 +379,23 @@ public:
protected: protected:
void closeEvent(QCloseEvent *event); void closeEvent(QCloseEvent *event);
void loadClientTree(void);
QTreeWidget *clientTree; QTreeWidget *clientTree;
QPushButton *dropPlayerButton;
QPushButton *resyncPlayerButton;
QPushButton *resyncAllButton;
QTimer *periodicTimer;
static NetPlayHostStatusDialog* instance; static NetPlayHostStatusDialog* instance;
public slots: public slots:
void closeWindow(void); void closeWindow(void);
void updatePeriodic(void);
void loadClientTree(void);
void onClientTreeContextMenu(const QPoint &pos);
void clientItemClicked(QTreeWidgetItem* item, int);
void dropPlayer(void);
void resyncPlayer(void);
void resyncAllPlayers(void);
}; };
bool NetPlayActive(void); bool NetPlayActive(void);

View File

@ -125,6 +125,7 @@ struct netPlayErrorMsg
netPlayErrorMsg(void) netPlayErrorMsg(void)
: hdr(NETPLAY_ERROR_MSG, sizeof(netPlayErrorMsg)), code(0), flags(0) : hdr(NETPLAY_ERROR_MSG, sizeof(netPlayErrorMsg)), code(0), flags(0)
{ {
hdr.msgSize = sizeof(*this) - N + 1;
memset(data, 0, N); memset(data, 0, N);
} }
@ -151,7 +152,7 @@ struct netPlayErrorMsg
retval = ::vsnprintf(data, sizeof(data), format, args); retval = ::vsnprintf(data, sizeof(data), format, args);
va_end(args); va_end(args);
hdr.msgSize = sizeof(netPlayErrorMsg) - N + strlen(data) + 1; hdr.msgSize = sizeof(*this) - N + strlen(data) + 1;
return retval; return retval;
} }
@ -239,7 +240,8 @@ struct netPlayClientState
uint32_t ramChkSum; uint32_t ramChkSum;
uint8_t ctrlState[4]; uint8_t ctrlState[4];
static constexpr uint32_t PAUSE_FLAG = 0x0001; static constexpr uint32_t PAUSE_FLAG = 0x0001;
static constexpr uint32_t DESYNC_FLAG = 0x0002;
netPlayClientState(void) netPlayClientState(void)
: hdr(NETPLAY_CLIENT_STATE, sizeof(netPlayClientState)), flags(0), : hdr(NETPLAY_CLIENT_STATE, sizeof(netPlayClientState)), flags(0),