diff --git a/Data/Sys/GameFiles/GALE01/MnMaAll.dat.diff b/Data/Sys/GameFiles/GALE01/MnMaAll.dat.diff index daf7b8a4f2..37611005bb 100644 Binary files a/Data/Sys/GameFiles/GALE01/MnMaAll.dat.diff and b/Data/Sys/GameFiles/GALE01/MnMaAll.dat.diff differ diff --git a/Data/Sys/GameFiles/GALE01/MnMaAll.usd.diff b/Data/Sys/GameFiles/GALE01/MnMaAll.usd.diff index d923e391ac..ee3ecaa706 100644 Binary files a/Data/Sys/GameFiles/GALE01/MnMaAll.usd.diff and b/Data/Sys/GameFiles/GALE01/MnMaAll.usd.diff differ diff --git a/Data/Sys/GameFiles/GALE01/MnSlChr.dat.diff b/Data/Sys/GameFiles/GALE01/MnSlChr.dat.diff index c0361596b5..3b642123f7 100644 Binary files a/Data/Sys/GameFiles/GALE01/MnSlChr.dat.diff and b/Data/Sys/GameFiles/GALE01/MnSlChr.dat.diff differ diff --git a/Data/Sys/GameFiles/GALE01/MnSlChr.usd.diff b/Data/Sys/GameFiles/GALE01/MnSlChr.usd.diff index 2fdbaa5659..1e0ec9013c 100644 Binary files a/Data/Sys/GameFiles/GALE01/MnSlChr.usd.diff and b/Data/Sys/GameFiles/GALE01/MnSlChr.usd.diff differ diff --git a/Data/Sys/GameFiles/GALE01/SdMenu.dat.diff b/Data/Sys/GameFiles/GALE01/SdMenu.dat.diff index 6cf3f9a781..d8ad96c263 100644 Binary files a/Data/Sys/GameFiles/GALE01/SdMenu.dat.diff and b/Data/Sys/GameFiles/GALE01/SdMenu.dat.diff differ diff --git a/Data/Sys/GameFiles/GALE01/SdMenu.usd.diff b/Data/Sys/GameFiles/GALE01/SdMenu.usd.diff index 89c39acd05..9f11127498 100644 Binary files a/Data/Sys/GameFiles/GALE01/SdMenu.usd.diff and b/Data/Sys/GameFiles/GALE01/SdMenu.usd.diff differ diff --git a/Data/Sys/GameFiles/GALE01/SdSlChr.dat.diff b/Data/Sys/GameFiles/GALE01/SdSlChr.dat.diff new file mode 100644 index 0000000000..65e8f46e77 Binary files /dev/null and b/Data/Sys/GameFiles/GALE01/SdSlChr.dat.diff differ diff --git a/Data/Sys/GameFiles/GALE01/SdSlChr.usd.diff b/Data/Sys/GameFiles/GALE01/SdSlChr.usd.diff new file mode 100644 index 0000000000..52d7471974 Binary files /dev/null and b/Data/Sys/GameFiles/GALE01/SdSlChr.usd.diff differ diff --git a/Source/Core/Core/ConfigManager.cpp b/Source/Core/Core/ConfigManager.cpp index d8a0b6b52e..0cebb583bd 100644 --- a/Source/Core/Core/ConfigManager.cpp +++ b/Source/Core/Core/ConfigManager.cpp @@ -258,6 +258,7 @@ void SConfig::SaveSlippiSettings(IniFile& ini) slippi->Set("OnlineDelay", m_slippiOnlineDelay); slippi->Set("SaveReplays", m_slippiSaveReplays); + slippi->Set("EnableQuickChat", m_slippiEnableQuickChat); slippi->Set("ReplayMonthFolders", m_slippiReplayMonthFolders); slippi->Set("ReplayDir", m_strSlippiReplayDir); slippi->Set("PlaybackControls", m_slippiEnableSeek); @@ -543,6 +544,7 @@ void SConfig::LoadSlippiSettings(IniFile& ini) slippi->Get("PlaybackControls", &m_slippiEnableSeek, true); slippi->Get("OnlineDelay", &m_slippiOnlineDelay, 2); slippi->Get("SaveReplays", &m_slippiSaveReplays, true); + slippi->Get("EnableQuickChat", &m_slippiEnableQuickChat, true); slippi->Get("ReplayMonthFolders", &m_slippiReplayMonthFolders, false); std::string default_replay_dir = File::GetHomeDirectory() + DIR_SEP + "Slippi"; slippi->Get("ReplayDir", &m_strSlippiReplayDir, default_replay_dir); diff --git a/Source/Core/Core/ConfigManager.h b/Source/Core/Core/ConfigManager.h index c44853a135..4c031c805d 100644 --- a/Source/Core/Core/ConfigManager.h +++ b/Source/Core/Core/ConfigManager.h @@ -154,6 +154,7 @@ struct SConfig int m_slippiOnlineDelay = 2; bool m_slippiEnableSeek = true; bool m_slippiSaveReplays = true; + bool m_slippiEnableQuickChat = true; bool m_slippiReplayMonthFolders = false; std::string m_strSlippiReplayDir; bool m_blockingPipes = false; diff --git a/Source/Core/Core/HW/EXI/EXI_DeviceSlippi.cpp b/Source/Core/Core/HW/EXI/EXI_DeviceSlippi.cpp index fd2ee405ef..fab97d884f 100644 --- a/Source/Core/Core/HW/EXI/EXI_DeviceSlippi.cpp +++ b/Source/Core/Core/HW/EXI/EXI_DeviceSlippi.cpp @@ -448,19 +448,15 @@ void CEXISlippi::writeToFile(std::unique_ptr msg) // Get display names and connection codes from slippi netplay client if (slippi_netplay) - { - auto userInfo = user->GetUserInfo(); - auto oppInfo = matchmaking->GetOpponent(); + { + auto playerInfo = matchmaking->GetPlayerInfo(); - auto isDecider = slippi_netplay->IsDecider(); - int local_port = isDecider ? 0 : 1; - int remote_port = isDecider ? 1 : 0; - - slippi_names[local_port] = userInfo.display_name; - slippi_connect_codes[local_port] = userInfo.connect_code; - slippi_names[remote_port] = oppInfo.display_name; - slippi_connect_codes[remote_port] = oppInfo.connect_code; - } + for (int i = 0; i < playerInfo.size(); i++) + { + slippi_names[i] = playerInfo[i].display_name; + slippi_connect_codes[i] = playerInfo[i].connect_code; + } + } } // If no file, do nothing @@ -1499,12 +1495,6 @@ void CEXISlippi::handleOnlineInputs(u8* payload) int32_t frame = payload[0] << 24 | payload[1] << 16 | payload[2] << 8 | payload[3]; - if (isDisconnected()) - { - m_read_queue.push_back(3); // Indicate we disconnected - return; - } - if (frame == 1) { availableSavestates.clear(); @@ -1525,6 +1515,13 @@ void CEXISlippi::handleOnlineInputs(u8* payload) slippi_netplay->StartSlippiGame(); } + if (isDisconnected()) + { + auto status = slippi_netplay->GetSlippiConnectStatus(); + m_read_queue.push_back(3); // Indicate we disconnected + return; + } + if (shouldSkipOnlineFrame(frame)) { // Send inputs that have not yet been acked @@ -1586,6 +1583,7 @@ bool CEXISlippi::shouldSkipOnlineFrame(s32 frame) auto offsetUs = slippi_netplay->CalcTimeOffsetUs(); INFO_LOG(SLIPPI_ONLINE, "[Frame %d] Offset is: %d us", frame, offsetUs); + // TODO: figure out a better solution here for doubles? if (offsetUs > 10000) { isCurrentlySkipping = true; @@ -1642,27 +1640,56 @@ void CEXISlippi::prepareOpponentInputs(u8* payload) m_read_queue.push_back(frameResult); // Indicate a continue frame - int32_t frame = payload[0] << 24 | payload[1] << 16 | payload[2] << 8 | payload[3]; + u8 remotePlayerCount = matchmaking->RemotePlayerCount(); + m_read_queue.push_back(remotePlayerCount); // Indicate the number of remote players - auto result = slippi_netplay->GetSlippiRemotePad(frame); + int32_t frame = payload[0] << 24 | payload[1] << 16 | payload[2] << 8 | payload[3]; - // determine offset from which to copy data - int offset = (result->latestFrame - frame) * SLIPPI_PAD_FULL_SIZE; - offset = offset < 0 ? 0 : offset; + std::unique_ptr results[SLIPPI_REMOTE_PLAYER_MAX]; + int offset[SLIPPI_REMOTE_PLAYER_MAX]; + INFO_LOG(SLIPPI_ONLINE, "Preparing pad data for frame %d", frame); - // add latest frame we are transfering to begining of return buf - int32_t latestFrame = offset > 0 ? frame : result->latestFrame; - appendWordToBuffer(&m_read_queue, *(u32*)&latestFrame); + // Get pad data for each remote player and write each of their latest frame nums to the buf + for (int i = 0; i < remotePlayerCount; i++) + { + results[i] = slippi_netplay->GetSlippiRemotePad(frame, i); - // copy pad data over - auto txStart = result->data.begin() + offset; - auto txEnd = result->data.end(); + // determine offset from which to copy data + offset[i] = (results[i]->latestFrame - frame) * SLIPPI_PAD_FULL_SIZE; + offset[i] = offset[i] < 0 ? 0 : offset[i]; - std::vector tx; - tx.insert(tx.end(), txStart, txEnd); - tx.resize(SLIPPI_PAD_FULL_SIZE * ROLLBACK_MAX_FRAMES, 0); + // add latest frame we are transfering to begining of return buf + int32_t latestFrame = results[i]->latestFrame; + if (latestFrame > frame) + latestFrame = frame; + appendWordToBuffer(&m_read_queue, *(u32 *)&latestFrame); + // INFO_LOG(SLIPPI_ONLINE, "Sending frame num %d for pIdx %d (offset: %d)", latestFrame, i, offset[i]); + } + // Send the current frame for any unused player slots. + for (int i = remotePlayerCount; i < SLIPPI_REMOTE_PLAYER_MAX; i++) + { + appendWordToBuffer(&m_read_queue, *(u32 *)&frame); + } - m_read_queue.insert(m_read_queue.end(), tx.begin(), tx.end()); + // copy pad data over + for (int i = 0; i < SLIPPI_REMOTE_PLAYER_MAX; i++) + { + std::vector tx; + + // Get pad data if this remote player exists + if (i < remotePlayerCount) + { + auto txStart = results[i]->data.begin() + offset[i]; + auto txEnd = results[i]->data.end(); + tx.insert(tx.end(), txStart, txEnd); + } + + tx.resize(SLIPPI_PAD_FULL_SIZE * ROLLBACK_MAX_FRAMES, 0); + + m_read_queue.insert(m_read_queue.end(), tx.begin(), tx.end()); + } + + slippi_netplay->DropOldRemoteInputs(frame); // ERROR_LOG(SLIPPI_ONLINE, "EXI: [%d] %X %X %X %X %X %X %X %X", latestFrame, m_read_queue[5], // m_read_queue[6], m_read_queue[7], m_read_queue[8], m_read_queue[9], m_read_queue[10], @@ -1771,7 +1798,7 @@ void CEXISlippi::startFindMatch(u8* payload) // give someone an early error before they even queue so that they wont enter the queue and make // someone else get force removed from queue and have to requeue auto directMode = SlippiMatchmaking::OnlinePlayMode::DIRECT; - if (search.mode != directMode && localSelections.characterId >= 26) + if (search.mode < directMode && localSelections.characterId >= 26) { forcedError = "The character you selected is not allowed in this mode"; return; @@ -1794,31 +1821,28 @@ void CEXISlippi::startFindMatch(u8* payload) void CEXISlippi::prepareOnlineMatchState() { - // This match block is a VS match with P1 Red Falco vs P2 Red Bowser on Battlefield. The proper - // values will be overwritten - static std::vector onlineMatchBlock = { - 0x32, 0x01, 0x86, 0x4C, 0xC3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x6E, 0x00, - 0x1F, 0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x3F, - 0x80, 0x00, 0x00, 0x3F, 0x80, 0x00, 0x00, 0x3F, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x09, - 0x00, 0x78, 0x00, 0xC0, 0x00, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x3F, 0x80, 0x00, 0x00, 0x3F, 0x80, 0x00, 0x00, 0x3F, 0x80, 0x00, 0x00, 0x05, 0x00, 0x04, - 0x01, 0x00, 0x01, 0x00, 0x00, 0x09, 0x00, 0x78, 0x00, 0xC0, 0x00, 0x04, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x80, 0x00, 0x00, 0x3F, 0x80, 0x00, 0x00, 0x3F, - 0x80, 0x00, 0x00, 0x1A, 0x03, 0x04, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x09, 0x00, 0x78, 0x00, - 0x40, 0x00, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x80, 0x00, - 0x00, 0x3F, 0x80, 0x00, 0x00, 0x3F, 0x80, 0x00, 0x00, 0x1A, 0x03, 0x04, 0x00, 0x00, 0xFF, - 0x00, 0x00, 0x09, 0x00, 0x78, 0x00, 0x40, 0x00, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x3F, 0x80, 0x00, 0x00, 0x3F, 0x80, 0x00, 0x00, 0x3F, 0x80, 0x00, 0x00, - 0x21, 0x03, 0x04, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x09, 0x00, 0x78, 0x00, 0x40, 0x00, 0x04, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x80, 0x00, 0x00, 0x3F, 0x80, - 0x00, 0x00, 0x3F, 0x80, 0x00, 0x00, 0x21, 0x03, 0x04, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x09, - 0x00, 0x78, 0x00, 0x40, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x3F, 0x80, 0x00, 0x00, 0x3F, 0x80, 0x00, 0x00, 0x3F, 0x80, 0x00, 0x00, - }; + // This match block is a VS match with P1 Red Falco vs P2 Red Bowser vs P3 Young Link vs P4 Young Link + // on Battlefield. The proper values will be overwritten + static std::vector onlineMatchBlock = { + 0x32, 0x01, 0x86, 0x4C, 0xC3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x6E, 0x00, 0x1F, 0x00, 0x00, + 0x01, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x80, 0x00, 0x00, 0x3F, 0x80, 0x00, 0x00, 0x3F, 0x80, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x09, 0x00, 0x78, 0x00, + 0xC0, 0x00, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x80, 0x00, 0x00, 0x3F, 0x80, + 0x00, 0x00, 0x3F, 0x80, 0x00, 0x00, 0x05, 0x00, 0x04, 0x01, 0x00, 0x01, 0x00, 0x00, 0x09, 0x00, 0x78, 0x00, + 0xC0, 0x00, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x80, 0x00, 0x00, 0x3F, 0x80, + 0x00, 0x00, 0x3F, 0x80, 0x00, 0x00, 0x15, 0x03, 0x04, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x09, 0x00, 0x78, 0x00, + 0xC0, 0x00, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x80, 0x00, 0x00, 0x3F, 0x80, + 0x00, 0x00, 0x3F, 0x80, 0x00, 0x00, 0x15, 0x03, 0x04, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x09, 0x00, 0x78, 0x00, + 0xC0, 0x00, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x80, 0x00, 0x00, 0x3F, 0x80, + 0x00, 0x00, 0x3F, 0x80, 0x00, 0x00, 0x21, 0x03, 0x04, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x09, 0x00, 0x78, 0x00, + 0x40, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x80, 0x00, 0x00, 0x3F, 0x80, + 0x00, 0x00, 0x3F, 0x80, 0x00, 0x00, 0x21, 0x03, 0x04, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x09, 0x00, 0x78, 0x00, + 0x40, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x80, 0x00, 0x00, 0x3F, 0x80, + 0x00, 0x00, 0x3F, 0x80, 0x00, 0x00, + }; m_read_queue.clear(); @@ -1838,12 +1862,10 @@ void CEXISlippi::prepareOnlineMatchState() m_read_queue.push_back(mmState); // Matchmaking State u8 localPlayerReady = localSelections.isCharacterSelected; - u8 remotePlayerReady = 0; - u8 localPlayerIndex = 0; + u8 remotePlayersReady = 0; + u8 localPlayerIndex = matchmaking->LocalPlayerIndex(); u8 remotePlayerIndex = 1; - auto opponent = matchmaking->GetOpponent(); - std::string oppName = opponent.display_name; auto userInfo = user->GetUserInfo(); if (mmState == SlippiMatchmaking::ProcessState::CONNECTION_SUCCESS) @@ -1871,9 +1893,24 @@ void CEXISlippi::prepareOnlineMatchState() { auto matchInfo = slippi_netplay->GetMatchInfo(); #ifdef LOCAL_TESTING - remotePlayerReady = true; + remotePlayersReady = true; #else - remotePlayerReady = matchInfo->remotePlayerSelections.isCharacterSelected; + remotePlayersReady = 1; + u8 remotePlayerCount = matchmaking->RemotePlayerCount(); + for (int i = 0; i < remotePlayerCount; i++) + { + if (!matchInfo->remotePlayerSelections[i].isCharacterSelected) + { + remotePlayersReady = 0; + } + } + + if (remotePlayerCount == 1) + { + auto isDecider = slippi_netplay->IsDecider(); + localPlayerIndex = isDecider ? 0 : 1; + remotePlayerIndex = isDecider ? 1 : 0; + } #endif auto isDecider = slippi_netplay->IsDecider(); @@ -1892,9 +1929,13 @@ void CEXISlippi::prepareOnlineMatchState() // Here we are connected, check to see if we should init play session if (!is_play_session_active) { - std::vector uids{"", ""}; - uids[localPlayerIndex] = userInfo.uid; - uids[remotePlayerIndex] = opponent.uid; + std::vector uids; + + auto mmPlayers = matchmaking->GetPlayerInfo(); + for (auto mmp : mmPlayers) + { + uids.push_back(mmp.uid); + } game_reporter->StartNewSession(uids); @@ -1906,55 +1947,93 @@ void CEXISlippi::prepareOnlineMatchState() slippi_netplay = nullptr; } - m_read_queue.push_back(localPlayerReady); // Local player ready - m_read_queue.push_back(remotePlayerReady); // Remote player ready - m_read_queue.push_back(localPlayerIndex); // Local player index - m_read_queue.push_back(remotePlayerIndex); // Remote player index - u32 rngOffset = 0; + std::string localPlayerName = ""; + std::string oppName = ""; std::string p1Name = ""; std::string p2Name = ""; u8 chatMessageId = 0; + u8 chatMessagePlayerIdx = 0; u8 sentChatMessageId = 0; #ifdef LOCAL_TESTING - chatMessageId = localChatMessageId; - localChatMessageId = 0; - // in CSS p1 is always current player and p2 is opponent - p1Name = "Player 1"; - p2Name = "Player 2"; + localPlayerIndex = 0; + chatMessageId = localChatMessageId; + chatMessagePlayerIdx = 0; + localChatMessageId = 0; + // in CSS p1 is always current player and p2 is opponent + localPlayerName = p1Name = "Player 1"; + oppName = p2Name = "Player 2"; #endif - // Set chat message if any - if (slippi_netplay) - { - chatMessageId = slippi_netplay->GetSlippiRemoteChatMessage(); - sentChatMessageId = slippi_netplay->GetSlippiRemoteSentChatMessage(); - // in CSS p1 is always current player and p2 is opponent - p1Name = userInfo.display_name; - p2Name = oppName; - } + m_read_queue.push_back(localPlayerReady); // Local player ready + m_read_queue.push_back(remotePlayersReady); // Remote players ready + m_read_queue.push_back(localPlayerIndex); // Local player index + m_read_queue.push_back(remotePlayerIndex); // Remote player index - auto directMode = SlippiMatchmaking::OnlinePlayMode::DIRECT; + // Set chat message if any + if (slippi_netplay) + { + auto remoteMessageSelection = slippi_netplay->GetSlippiRemoteChatMessage(); + chatMessageId = remoteMessageSelection.messageId; + chatMessagePlayerIdx = remoteMessageSelection.playerIdx; + sentChatMessageId = slippi_netplay->GetSlippiRemoteSentChatMessage(); + // in CSS p1 is always current player and p2 is opponent + localPlayerName = p1Name = userInfo.display_name; + } - if (localPlayerReady && remotePlayerReady) - { - auto isDecider = slippi_netplay->IsDecider(); + auto directMode = SlippiMatchmaking::OnlinePlayMode::DIRECT; - auto matchInfo = slippi_netplay->GetMatchInfo(); - SlippiPlayerSelections lps = matchInfo->localPlayerSelections; - SlippiPlayerSelections rps = matchInfo->remotePlayerSelections; + std::vector leftTeamPlayers = {}; + std::vector rightTeamPlayers = {}; + + if (localPlayerReady && remotePlayersReady) + { + auto isDecider = slippi_netplay->IsDecider(); + u8 remotePlayerCount = matchmaking->RemotePlayerCount(); + auto matchInfo = slippi_netplay->GetMatchInfo(); + SlippiPlayerSelections lps = matchInfo->localPlayerSelections; + auto rps = matchInfo->remotePlayerSelections; #ifdef LOCAL_TESTING - rps.characterId = 0x2; - rps.characterColor = 2; - oppName = std::string("Player"); + lps.playerIdx = 0; + + for (int i = 0; i < SLIPPI_REMOTE_PLAYER_MAX; i++) + { + if (i == 0) + { + rps[i].characterColor = 1; + rps[i].teamId = 1; + } + else + { + rps[i].characterColor = 2; + rps[i].teamId = 2; + } + + rps[i].characterId = 0x2 + i; + rps[i].playerIdx = i + 1; + rps[i].isCharacterSelected = true; + } + + if (lastSearch.mode == SlippiMatchmaking::OnlinePlayMode::TEAMS) + { + remotePlayerCount = 4; + } + + oppName = std::string("Player"); #endif - // Check if someone is picking dumb characters in non-direct - auto localCharOk = lps.characterId < 26; - auto remoteCharOk = rps.characterId < 26; - if (lastSearch.mode != directMode && (!localCharOk || !remoteCharOk)) + // Check if someone is picking dumb characters in non-direct + auto localCharOk = lps.characterId < 26; + auto remoteCharOk = true; + INFO_LOG(SLIPPI_ONLINE, "remotePlayerCount: %d", remotePlayerCount); + for (int i = 0; i < remotePlayerCount; i++) + { + if (rps[i].characterId >= 26) + remoteCharOk = false; + } + if (lastSearch.mode < directMode && (!localCharOk || !remoteCharOk)) { // If we get here, someone is doing something bad, clear the lobby handleConnectionCleanup(); @@ -1965,52 +2044,99 @@ void CEXISlippi::prepareOnlineMatchState() } // Overwrite local player character - onlineMatchBlock[0x60 + localPlayerIndex * 0x24] = lps.characterId; - onlineMatchBlock[0x63 + localPlayerIndex * 0x24] = lps.characterColor; + onlineMatchBlock[0x60 + (lps.playerIdx) * 0x24] = lps.characterId; + onlineMatchBlock[0x63 + (lps.playerIdx) * 0x24] = lps.characterColor; + onlineMatchBlock[0x67 + (lps.playerIdx) * 0x24] = 0; + onlineMatchBlock[0x69 + (lps.playerIdx) * 0x24] = lps.teamId; - // Overwrite remote player character - onlineMatchBlock[0x60 + remotePlayerIndex * 0x24] = rps.characterId; - onlineMatchBlock[0x63 + remotePlayerIndex * 0x24] = rps.characterColor; + // Overwrite remote player character + for (int i = 0; i < remotePlayerCount; i++) + { + u8 idx = matchInfo->remotePlayerSelections[i].playerIdx; + onlineMatchBlock[0x60 + idx * 0x24] = matchInfo->remotePlayerSelections[i].characterId; - // Make one character lighter if same character, same color - bool isSheikVsZelda = lps.characterId == 0x12 && rps.characterId == 0x13 || - lps.characterId == 0x13 && rps.characterId == 0x12; - bool charMatch = lps.characterId == rps.characterId || isSheikVsZelda; - bool colMatch = lps.characterColor == rps.characterColor; + // Set Char Colors + onlineMatchBlock[0x63 + idx * 0x24] = matchInfo->remotePlayerSelections[i].characterColor; - onlineMatchBlock[0x67 + 0x24] = charMatch && colMatch ? 1 : 0; + // Set Team Ids + onlineMatchBlock[0x69 + idx * 0x24] = matchInfo->remotePlayerSelections[i].teamId; + } - // Overwrite stage - u16 stageId; - if (isDecider) - { - stageId = lps.isStageSelected ? lps.stageId : rps.stageId; - } - else - { - stageId = rps.isStageSelected ? rps.stageId : lps.stageId; - } + // Handle Singles/Teams specific logic + if (remotePlayerCount < 3) + { + onlineMatchBlock[0x8] = 0; // is Teams = false - // int seconds = 0; - // u32 *timer = (u32 *)&onlineMatchBlock[0x10]; - //*timer = Common::swap32(seconds * 60); + // Set p3/p4 player type to none + onlineMatchBlock[0x61 + 2 * 0x24] = 3; + onlineMatchBlock[0x61 + 3 * 0x24] = 3; - u16* stage = (u16*)&onlineMatchBlock[0xE]; - *stage = Common::swap16(stageId); + // Make one character lighter if same character, same color + bool isSheikVsZelda = lps.characterId == 0x12 && rps[0].characterId == 0x13 || + lps.characterId == 0x13 && rps[0].characterId == 0x12; + bool charMatch = lps.characterId == rps[0].characterId || isSheikVsZelda; + bool colMatch = lps.characterColor == rps[0].characterColor; - // Set rng offset - rngOffset = isDecider ? lps.rngOffset : rps.rngOffset; - WARN_LOG(SLIPPI_ONLINE, "Rng Offset: 0x%x", rngOffset); - WARN_LOG(SLIPPI_ONLINE, "P1 Char: 0x%X, P2 Char: 0x%X", onlineMatchBlock[0x60], - onlineMatchBlock[0x84]); + onlineMatchBlock[0x67 + 0x24] = charMatch && colMatch ? 1 : 0; + } + else + { + onlineMatchBlock[0x8] = 1; // is Teams = true - // Set player names - p1Name = isDecider ? userInfo.display_name : oppName; - p2Name = isDecider ? oppName : userInfo.display_name; + // Set p3/p4 player type to human + onlineMatchBlock[0x61 + 2 * 0x24] = 0; + onlineMatchBlock[0x61 + 3 * 0x24] = 0; - // Turn pause on in direct, off in everything else - u8* gameBitField3 = (u8*)&onlineMatchBlock[2]; - *gameBitField3 = lastSearch.mode == directMode ? *gameBitField3 & 0xF7 : *gameBitField3 | 0x8; + // Set alt color to light/dark costume for multiples of the same character on a team + int characterCount[26][3] = {0}; + for (int i = 0; i < 4; i++) + { + int charId = onlineMatchBlock[0x60 + i * 0x24]; + int teamId = onlineMatchBlock[0x69 + i * 0x24]; + onlineMatchBlock[0x67 + i * 0x24] = characterCount[charId][teamId]; + characterCount[charId][teamId]++; + } + } + + // Overwrite stage + u16 stageId; + if (isDecider) + { + stageId = lps.isStageSelected ? lps.stageId : rps[0].stageId; + } + else + { + stageId = rps[0].isStageSelected ? rps[0].stageId : lps.stageId; + } + + u16 *stage = (u16 *)&onlineMatchBlock[0xE]; + *stage = Common::swap16(stageId); + + // Set rng offset + rngOffset = isDecider ? lps.rngOffset : rps[0].rngOffset; + WARN_LOG(SLIPPI_ONLINE, "Rng Offset: 0x%x", rngOffset); + WARN_LOG(SLIPPI_ONLINE, "P1 Char: 0x%X, P2 Char: 0x%X", onlineMatchBlock[0x60], onlineMatchBlock[0x84]); + + // Turn pause on in direct, off in everything else + u8 *gameBitField3 = (u8 *)&onlineMatchBlock[2]; + *gameBitField3 = lastSearch.mode >= directMode ? *gameBitField3 & 0xF7 : *gameBitField3 | 0x8; + //*gameBitField3 = *gameBitField3 | 0x8; + + // Group players into left/right side for team splash screen display + for (int i = 0; i < 4; i++) + { + int teamId = onlineMatchBlock[0x69 + i * 0x24]; + if (teamId == lps.teamId) + leftTeamPlayers.push_back(i); + else + rightTeamPlayers.push_back(i); + } + int leftTeamSize = leftTeamPlayers.size(); + int rightTeamSize = rightTeamPlayers.size(); + leftTeamPlayers.resize(4, 0); + rightTeamPlayers.resize(4, 0); + leftTeamPlayers[3] = leftTeamSize; + rightTeamPlayers[3] = rightTeamSize; } // Add rng offset to output @@ -2022,13 +2148,52 @@ void CEXISlippi::prepareOnlineMatchState() // Add chat messages id m_read_queue.push_back((u8)sentChatMessageId); m_read_queue.push_back((u8)chatMessageId); +m_read_queue.push_back((u8)chatMessagePlayerIdx); - // Add names to output - p1Name = ConvertStringForGame(p1Name, MAX_NAME_LENGTH); - m_read_queue.insert(m_read_queue.end(), p1Name.begin(), p1Name.end()); - p2Name = ConvertStringForGame(p2Name, MAX_NAME_LENGTH); - m_read_queue.insert(m_read_queue.end(), p2Name.begin(), p2Name.end()); - oppName = ConvertStringForGame(oppName, MAX_NAME_LENGTH); + // Add player groupings for VS splash screen + leftTeamPlayers.resize(4, 0); + rightTeamPlayers.resize(4, 0); + m_read_queue.insert(m_read_queue.end(), leftTeamPlayers.begin(), leftTeamPlayers.end()); + m_read_queue.insert(m_read_queue.end(), rightTeamPlayers.begin(), rightTeamPlayers.end()); + + // Add names to output + // Always send static local player name + localPlayerName = ConvertStringForGame(localPlayerName, MAX_NAME_LENGTH); + m_read_queue.insert(m_read_queue.end(), localPlayerName.begin(), localPlayerName.end()); + +#ifdef LOCAL_TESTING + std::string defaultNames[] = {"Player 1", "Player 2", "Player 3", "Player 4"}; +#endif + + for (int i = 0; i < 4; i++) + { + std::string name = matchmaking->GetPlayerName(i); +#ifdef LOCAL_TESTING + name = defaultNames[i]; +#endif + name = ConvertStringForGame(name, MAX_NAME_LENGTH); + m_read_queue.insert(m_read_queue.end(), name.begin(), name.end()); + } + + // Create the opponent string using the names of all players on opposing teams + int teamIdx = onlineMatchBlock[0x69 + localPlayerIndex * 0x24]; + std::string oppText = ""; + for (int i = 0; i < 4; i++) + { + if (i == localPlayerIndex) + continue; + + if (onlineMatchBlock[0x69 + i * 0x24] != teamIdx) + { + if (oppText != "") + oppText += "/"; + + oppText += matchmaking->GetPlayerName(i); + } + } + if (matchmaking->RemotePlayerCount() == 1) + oppText = matchmaking->GetPlayerName(remotePlayerIndex); + oppName = ConvertStringForGame(oppText, MAX_NAME_LENGTH * 2 + 1); m_read_queue.insert(m_read_queue.end(), oppName.begin(), oppName.end()); // Add error message if there is one @@ -2071,12 +2236,13 @@ void CEXISlippi::setMatchSelections(u8* payload) { SlippiPlayerSelections s; - s.characterId = payload[0]; - s.characterColor = payload[1]; - s.isCharacterSelected = payload[2]; + s.teamId = payload[0]; + s.characterId = payload[1]; + s.characterColor = payload[2]; + s.isCharacterSelected = payload[3]; - s.stageId = Common::swap16(&payload[3]); - u8 stageSelectOption = payload[5]; + s.stageId = Common::swap16(&payload[4]); + u8 stageSelectOption = payload[6]; s.isStageSelected = stageSelectOption == 1 || stageSelectOption == 3; if (stageSelectOption == 3) @@ -2085,7 +2251,16 @@ void CEXISlippi::setMatchSelections(u8* payload) s.stageId = getRandomStage(); } - s.rngOffset = generator() % 0xFFFF; + INFO_LOG(SLIPPI, "LPS set char: %d, iSS: %d, %d, stage: %d, team: %d", s.isCharacterSelected, stageSelectOption, + s.isStageSelected, s.stageId, s.teamId); + + s.rngOffset = generator() % 0xFFFF; + + if (matchmaking->LocalPlayerIndex() == 1 && firstMatch) + { + firstMatch = false; + s.stageId = getRandomStage(); + } // Merge these selections localSelections.Merge(s); @@ -2142,7 +2317,7 @@ void CEXISlippi::handleChatMessage(u8* payload) auto packet = std::make_unique(); // OSD::AddMessage("[Me]: "+ msg, OSD::Duration::VERY_LONG, OSD::Color::YELLOW); slippi_netplay->remoteSentChatMessageId = messageId; - slippi_netplay->WriteChatMessageToPacket(*packet, messageId); + slippi_netplay->WriteChatMessageToPacket(*packet, messageId, slippi_netplay->LocalPlayerPort()); slippi_netplay->SendAsync(std::move(packet)); } } @@ -2258,6 +2433,7 @@ void CEXISlippi::handleConnectionCleanup() // Reset play session is_play_session_active = false; + firstMatch = true; #ifdef LOCAL_TESTING isLocalConnected = false; diff --git a/Source/Core/Core/HW/EXI/EXI_DeviceSlippi.h b/Source/Core/Core/HW/EXI/EXI_DeviceSlippi.h index 2ab6de7cf1..c905443ff4 100644 --- a/Source/Core/Core/HW/EXI/EXI_DeviceSlippi.h +++ b/Source/Core/Core/HW/EXI/EXI_DeviceSlippi.h @@ -189,6 +189,7 @@ private: void logMessageFromGame(u8* payload); void prepareFileLength(u8* payload); void prepareFileLoad(u8* payload); + int getCharColor(u8 charId, u8 teamId); void FileWriteThread(void); @@ -214,6 +215,7 @@ private: u32 frameSeqIdx = 0; bool isEnetInitialized = false; + bool firstMatch = true; std::default_random_engine generator; diff --git a/Source/Core/Core/Slippi/SlippiMatchmaking.cpp b/Source/Core/Core/Slippi/SlippiMatchmaking.cpp index 206812a47f..04810384a0 100644 --- a/Source/Core/Core/Slippi/SlippiMatchmaking.cpp +++ b/Source/Core/Core/Slippi/SlippiMatchmaking.cpp @@ -7,6 +7,13 @@ #include "Common/StringUtil.h" #include "Common/Version.h" +#if defined __linux__ && HAVE_ALSA +#elif defined __APPLE__ +#include +#include +#elif defined _WIN32 +#endif + class MmMessageType { public: @@ -68,11 +75,6 @@ std::string SlippiMatchmaking::GetErrorMessage() return m_errorMsg; } -SlippiUser::UserInfo SlippiMatchmaking::GetOpponent() -{ - return m_oppUser; -} - bool SlippiMatchmaking::IsSearching() { return searchingStates.count(m_state) != 0; @@ -208,9 +210,13 @@ void SlippiMatchmaking::startMatchmaking() m_client = nullptr; int retryCount = 0; + auto userInfo = m_user->GetUserInfo(); while (m_client == nullptr && retryCount < 15) { - m_hostPort = 49000 + (generator() % 2000); + if (userInfo.port > 0) + m_hostPort = userInfo.port; + else + m_hostPort = 49000 + (generator() % 2000); ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Port to use: %d...", m_hostPort); // We are explicitly setting the client address because we are trying to utilize our connection @@ -269,6 +275,7 @@ void SlippiMatchmaking::startMatchmaking() continue; } + netEvent.peer->data = &userInfo.display_name; m_client->intercept = ENetUtil::InterceptCallback; isMmConnected = true; ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Connected to mm server..."); @@ -276,15 +283,40 @@ void SlippiMatchmaking::startMatchmaking() ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Trying to find match..."); - if (!m_user->IsLoggedIn()) - { - ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Must be logged in to queue"); - m_state = ProcessState::ERROR_ENCOUNTERED; - m_errorMsg = "Must be logged in to queue. Go back to menu"; - return; - } + // if (!m_user->IsLoggedIn()) + // { + // ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Must be logged in to queue"); + // m_state = ProcessState::ERROR_ENCOUNTERED; + // m_errorMsg = "Must be logged in to queue. Go back to menu"; + // return; + // } - auto userInfo = m_user->GetUserInfo(); + // Compute LAN IP, in case 2 people are connecting from one IP we can send them each other's local + // IP instead of public. Experimental to allow people from behind one router to connect. + char host[256]; + char lan_addr[30]; + char* ip; + struct hostent* host_entry; + int hostname; + hostname = gethostname(host, sizeof(host)); // find the host name + if (hostname == -1) + { + ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Error finding LAN address"); + } + else + { + host_entry = gethostbyname(host); // find host information + if (host_entry == NULL) + { + ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Error finding LAN host"); + } + else + { + ip = inet_ntoa(*((struct in_addr*)host_entry->h_addr_list[0])); // Convert into IP string + INFO_LOG(SLIPPI_ONLINE, "[Matchmaking] LAN IP: %s", ip); + sprintf(lan_addr, "%s:%d", ip, m_hostPort); + } + } std::vector connectCodeBuf; connectCodeBuf.insert(connectCodeBuf.end(), m_searchSettings.connectCode.begin(), @@ -296,6 +328,7 @@ void SlippiMatchmaking::startMatchmaking() request["user"] = {{"uid", userInfo.uid}, {"playKey", userInfo.play_key}}; request["search"] = {{"mode", m_searchSettings.mode}, {"connectCode", connectCodeBuf}}; request["appVersion"] = Common::scm_slippi_semver_str; + request["ipAddressLan"] = lan_addr; sendMessage(request); // Get response from server @@ -385,37 +418,64 @@ void SlippiMatchmaking::handleMatchmaking() m_isSwapAttempt = false; m_netplayClient = nullptr; - // Clear old user - SlippiUser::UserInfo emptyInfo; - m_oppUser = emptyInfo; + // Clear old users + m_remoteIps.clear(); + m_playerInfo.clear(); auto queue = getResp["players"]; - if (queue.is_array()) - { - for (json::iterator it = queue.begin(); it != queue.end(); ++it) - { - json el = *it; - //SlippiUser::UserInfo playerInfo; + if (queue.is_array()) + { + std::string localExternalIp = ""; - bool isLocal = el.value("isLocalPlayer", false); - //playerInfo.uid = el.value("uid", ""); - //playerInfo.displayName = el.value("displayName", ""); - //playerInfo.connectCode = el.value("connectCode", ""); - //playerInfo.port = el.value("port", 0); + for (json::iterator it = queue.begin(); it != queue.end(); ++it) + { + json el = *it; + SlippiUser::UserInfo playerInfo; - if (!isLocal) - { - m_oppIp = el.value("ipAddress", "1.1.1.1:123"); - m_oppUser.uid = el.value("uid", ""); - m_oppUser.display_name = el.value("displayName", ""); - m_oppUser.connect_code = el.value("connectCode", ""); - } + bool isLocal = el.value("isLocalPlayer", false); + playerInfo.uid = el.value("uid", ""); + playerInfo.display_name = el.value("displayName", ""); + playerInfo.connect_code = el.value("connectCode", ""); + playerInfo.port = el.value("port", 0); + m_playerInfo.push_back(playerInfo); - //else - // m_localPlayerPort = playerInfo.port - 1; - }; - } - m_isHost = getResp.value("isHost", false); + if (isLocal) + { + std::vector localIpParts; + localIpParts = SplitString(el.value("ipAddress", "1.1.1.1:123"), ':'); + localExternalIp = localIpParts[0]; + m_localPlayerIndex = playerInfo.port - 1; + } + }; + + // Loop a second time to get the correct remote IPs + for (json::iterator it = queue.begin(); it != queue.end(); ++it) + { + json el = *it; + + if (el.value("port", 0) - 1 == m_localPlayerIndex) + continue; + + auto extIp = el.value("ipAddress", "1.1.1.1:123"); + std::vector exIpParts; + exIpParts = SplitString(extIp, ':'); + + auto lanIp = el.value("ipAddressLan", "1.1.1.1:123"); + + if (exIpParts[0] != localExternalIp || lanIp.empty()) + { + // If external IPs are different, just use that address + m_remoteIps.push_back(extIp); + continue; + } + + // TODO: Instead of using one or the other, it might be better to try both + + // If external IPs are the same, try using LAN IPs + m_remoteIps.push_back(lanIp); + } + } + m_isHost = getResp.value("isHost", false); // Disconnect and destroy enet client to mm server terminateMmConnection(); @@ -425,13 +485,62 @@ void SlippiMatchmaking::handleMatchmaking() m_isHost ? "true" : "false"); } +int SlippiMatchmaking::LocalPlayerIndex() +{ + return m_localPlayerIndex; +} + +std::vector SlippiMatchmaking::GetPlayerInfo() +{ + return m_playerInfo; +} + +std::string SlippiMatchmaking::GetPlayerName(u8 port) +{ + if (port >= m_playerInfo.size()) + { + return ""; + } + return m_playerInfo[port].display_name; +} + +u8 SlippiMatchmaking::RemotePlayerCount() +{ + if (m_playerInfo.size() == 0) + return 0; + + return (u8)m_playerInfo.size() - 1; +} + void SlippiMatchmaking::handleConnecting() { - std::vector ipParts = SplitString(m_oppIp, ':'); + auto userInfo = m_user->GetUserInfo(); + + m_isSwapAttempt = false; + m_netplayClient = nullptr; + + u8 remotePlayerCount = (u8)m_remoteIps.size(); + std::vector remoteParts; + std::vector addrs; + std::vector ports; + for (int i = 0; i < m_remoteIps.size(); i++) + { + remoteParts.clear(); + remoteParts = SplitString(m_remoteIps[i], ':'); + addrs.push_back(remoteParts[0]); + ports.push_back(std::stoi(remoteParts[1])); + } + + std::stringstream ipLog; + ipLog << "Remote player IPs: "; + for (int i = 0; i < m_remoteIps.size(); i++) + { + ipLog << m_remoteIps[i] << ", "; + } // Is host is now used to specify who the decider is - auto client = std::make_unique(ipParts[0], std::stoi(ipParts[1]), m_hostPort, - m_isHost); + auto client = std::make_unique(addrs, ports, remotePlayerCount, m_hostPort, + m_isHost, m_localPlayerIndex); while (!m_netplayClient) { @@ -447,6 +556,32 @@ void SlippiMatchmaking::handleConnecting() continue; } + else if (status == SlippiNetplayClient::SlippiConnectStatus::NET_CONNECT_STATUS_FAILED && + m_searchSettings.mode == SlippiMatchmaking::OnlinePlayMode::TEAMS) + { + // If we failed setting up a connection in teams mode, show a detailed error about who we had + // issues connecting to. + ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Failed to connect to players"); + m_state = ProcessState::ERROR_ENCOUNTERED; + m_errorMsg = "Timed out waiting for other players to connect"; + auto failedConns = client->GetFailedConnections(); + if (!failedConns.empty()) + { + std::stringstream err; + err << "Could not connect to players: "; + for (int i = 0; i < failedConns.size(); i++) + { + int p = failedConns[i]; + if (p >= m_localPlayerIndex) + p++; + + err << m_playerInfo[p].display_name << " "; + } + m_errorMsg = err.str(); + } + + return; + } else if (status != SlippiNetplayClient::SlippiConnectStatus::NET_CONNECT_STATUS_CONNECTED) { ERROR_LOG(SLIPPI_ONLINE, diff --git a/Source/Core/Core/Slippi/SlippiMatchmaking.h b/Source/Core/Core/Slippi/SlippiMatchmaking.h index be4b042a51..96df53181e 100644 --- a/Source/Core/Core/Slippi/SlippiMatchmaking.h +++ b/Source/Core/Core/Slippi/SlippiMatchmaking.h @@ -5,11 +5,15 @@ #include "Core/Slippi/SlippiNetplay.h" #include "Core/Slippi/SlippiUser.h" -#include #include #include #include +#ifndef _WIN32 +#include +#include +#endif + #include using json = nlohmann::json; @@ -24,6 +28,7 @@ public: RANKED = 0, UNRANKED = 1, DIRECT = 2, + TEAMS = 3, }; enum ProcessState @@ -48,7 +53,10 @@ public: bool IsSearching(); std::unique_ptr GetNetplayClient(); std::string GetErrorMessage(); - SlippiUser::UserInfo GetOpponent(); + int LocalPlayerIndex(); + std::vector GetPlayerInfo(); + std::string GetPlayerName(u8 port); + u8 RemotePlayerCount(); protected: const std::string MM_HOST_DEV = "35.197.121.196"; // Dev host @@ -76,9 +84,11 @@ protected: int m_isSwapAttempt = false; int m_hostPort; - std::string m_oppIp; + int m_localPlayerIndex; + std::vector m_remoteIps; + std::vector m_playerInfo; + bool m_joinedLobby; bool m_isHost; - SlippiUser::UserInfo m_oppUser; std::unique_ptr m_netplayClient; diff --git a/Source/Core/Core/Slippi/SlippiNetplay.cpp b/Source/Core/Core/Slippi/SlippiNetplay.cpp index 568cfc3514..631c29f1e1 100644 --- a/Source/Core/Core/Slippi/SlippiNetplay.cpp +++ b/Source/Core/Core/Slippi/SlippiNetplay.cpp @@ -34,7 +34,7 @@ SlippiNetplayClient::~SlippiNetplayClient() if (m_thread.joinable()) m_thread.join(); - if (m_server) + if (!m_server.empty()) { Disconnect(); } @@ -55,8 +55,9 @@ SlippiNetplayClient::~SlippiNetplayClient() } // called from ---SLIPPI EXI--- thread -SlippiNetplayClient::SlippiNetplayClient(const std::string& address, const u16 remotePort, - const u16 localPort, bool isDecider) +SlippiNetplayClient::SlippiNetplayClient(std::vector addrs, std::vector ports, + const u8 remotePlayerCount, const u16 localPort, + bool isDecider, u8 playerIdx) #ifdef _WIN32 : m_qos_handle(nullptr), m_qos_flow_id(0) #endif @@ -65,6 +66,25 @@ SlippiNetplayClient::SlippiNetplayClient(const std::string& address, const u16 r isDecider ? "true" : "false"); this->isDecider = isDecider; + this->m_remotePlayerCount = remotePlayerCount; + this->playerIdx = playerIdx; + + // Set up remote player data structures. + int j = 0; + for (int i = 0; i < SLIPPI_REMOTE_PLAYER_MAX; i++, j++) + { + if (j == playerIdx) + j++; + this->matchInfo.remotePlayerSelections[i] = SlippiPlayerSelections(); + this->matchInfo.remotePlayerSelections[i].playerIdx = j; + + this->remotePadQueue[i] = std::deque>(); + this->frameOffsetData[i] = FrameOffsetData(); + this->lastFrameTiming[i] = FrameTiming(); + this->pingUs[i] = 0; + this->lastFrameAcked[i] = 0; + } + SLIPPI_NETPLAY = std::move(this); // Local address @@ -87,22 +107,32 @@ SlippiNetplayClient::SlippiNetplayClient(const std::string& address, const u16 r // TODO: Figure out how to use a local port when not hosting without accepting incoming // connections - m_client = enet_host_create(localAddr, 2, 3, 0, 0); + m_client = enet_host_create(localAddr, 10, 3, 0, 0); if (m_client == nullptr) { PanicAlertT("Couldn't Create Client"); } - ENetAddress addr; - enet_address_set_host(&addr, address.c_str()); - addr.port = remotePort; - - m_server = enet_host_connect(m_client, &addr, 3, 0); - - if (m_server == nullptr) + for (int i = 0; i < remotePlayerCount; i++) { - PanicAlertT("Couldn't create peer."); + ENetAddress addr; + enet_address_set_host(&addr, addrs[i].c_str()); + addr.port = ports[i]; + INFO_LOG(SLIPPI_ONLINE, "Set ENet host, addr = %x, port = %d", addr.host, addr.port); + + ENetPeer* peer = enet_host_connect(m_client, &addr, 3, 0); + m_server.push_back(peer); + + if (peer == nullptr) + { + PanicAlertT("Couldn't create peer."); + } + else + { + INFO_LOG(SLIPPI_ONLINE, "Connecting to ENet host, addr = %x, port = %d", peer->address.host, + peer->address.port); + } } slippiConnectStatus = SlippiConnectStatus::NET_CONNECT_STATUS_INITIATED; @@ -118,8 +148,23 @@ SlippiNetplayClient::SlippiNetplayClient(bool isDecider) slippiConnectStatus = SlippiConnectStatus::NET_CONNECT_STATUS_FAILED; } +u8 SlippiNetplayClient::PlayerIdxFromPort(u8 port) +{ + u8 p = port; + if (port > playerIdx) + { + p--; + } + return p; +} + +u8 SlippiNetplayClient::LocalPlayerPort() +{ + return this->playerIdx; +} + // called from ---NETPLAY--- thread -unsigned int SlippiNetplayClient::OnData(sf::Packet& packet) +unsigned int SlippiNetplayClient::OnData(sf::Packet& packet, ENetPeer* peer) { NetPlay::MessageId mid = 0; if (!(packet >> mid)) @@ -139,6 +184,19 @@ unsigned int SlippiNetplayClient::OnData(sf::Packet& packet) break; } + u8 packetPlayerPort; + if (!(packet >> packetPlayerPort)) + { + ERROR_LOG(SLIPPI_ONLINE, "Netplay packet too small to read player index"); + break; + } + u8 pIdx = PlayerIdxFromPort(packetPlayerPort); + if (pIdx >= m_remotePlayerCount) + { + ERROR_LOG(SLIPPI_ONLINE, "Got packet with invalid player idx %d", pIdx); + break; + } + // Pad received, try to guess what our local time was when the frame was sent by our opponent // before we initialized // We can compare this to when we sent a pad for last frame to figure out how far/behind we @@ -146,7 +204,7 @@ unsigned int SlippiNetplayClient::OnData(sf::Packet& packet) u64 curTime = Common::Timer::GetTimeUs(); - auto timing = lastFrameTiming; + auto timing = lastFrameTiming[pIdx]; if (!hasGameStarted) { // Handle case where opponent starts sending inputs before our game has reached frame 1. This @@ -155,7 +213,7 @@ unsigned int SlippiNetplayClient::OnData(sf::Packet& packet) timing.timeUs = curTime; } - s64 opponentSendTimeUs = curTime - (pingUs / 2); + s64 opponentSendTimeUs = curTime - (pingUs[pIdx] / 2); s64 frameDiffOffsetUs = 16683 * (timing.frame - frame); s64 timeOffsetUs = opponentSendTimeUs - timing.timeUs + frameDiffOffsetUs; @@ -163,12 +221,12 @@ unsigned int SlippiNetplayClient::OnData(sf::Packet& packet) timing.frame, timeOffsetUs); // Add this offset to circular buffer for use later - if (frameOffsetData.buf.size() < SLIPPI_ONLINE_LOCKSTEP_INTERVAL) - frameOffsetData.buf.push_back((s32)timeOffsetUs); + if (frameOffsetData[pIdx].buf.size() < SLIPPI_ONLINE_LOCKSTEP_INTERVAL) + frameOffsetData[pIdx].buf.push_back((s32)timeOffsetUs); else - frameOffsetData.buf[frameOffsetData.idx] = (s32)timeOffsetUs; + frameOffsetData[pIdx].buf[frameOffsetData[pIdx].idx] = (s32)timeOffsetUs; - frameOffsetData.idx = (frameOffsetData.idx + 1) % SLIPPI_ONLINE_LOCKSTEP_INTERVAL; + frameOffsetData[pIdx].idx = (frameOffsetData[pIdx].idx + 1) % SLIPPI_ONLINE_LOCKSTEP_INTERVAL; { std::lock_guard lk(pad_mutex); // TODO: Is this the correct lock? @@ -177,7 +235,10 @@ unsigned int SlippiNetplayClient::OnData(sf::Packet& packet) INFO_LOG(SLIPPI_ONLINE, "Receiving a packet of inputs [%d]...", frame); - int32_t headFrame = remotePadQueue.empty() ? 0 : remotePadQueue.front()->frame; + INFO_LOG(SLIPPI_ONLINE, "Receiving a packet of inputs from player %d(%d) [%d]...", + packetPlayerPort, pIdx, frame); + + int32_t headFrame = remotePadQueue[pIdx].empty() ? 0 : remotePadQueue[pIdx].front()->frame; int inputsToCopy = frame - headFrame; // Check that the packet actually contains the data it claims to @@ -192,13 +253,12 @@ unsigned int SlippiNetplayClient::OnData(sf::Packet& packet) for (int i = inputsToCopy - 1; i >= 0; i--) { auto pad = - std::make_unique(frame - i, &packetData[5 + i * SLIPPI_PAD_DATA_SIZE]); - + std::make_unique(frame - i, pIdx, &packetData[6 + i * SLIPPI_PAD_DATA_SIZE]); INFO_LOG(SLIPPI_ONLINE, "Rcv [%d] -> %02X %02X %02X %02X %02X %02X %02X %02X", pad->frame, pad->padBuf[0], pad->padBuf[1], pad->padBuf[2], pad->padBuf[3], pad->padBuf[4], pad->padBuf[5], pad->padBuf[6], pad->padBuf[7]); - remotePadQueue.push_front(std::move(pad)); + remotePadQueue[pIdx].push_front(std::move(pad)); } } @@ -206,7 +266,13 @@ unsigned int SlippiNetplayClient::OnData(sf::Packet& packet) sf::Packet spac; spac << (NetPlay::MessageId)NetPlay::NP_MSG_SLIPPI_PAD_ACK; spac << frame; - Send(spac); + spac << playerIdx; + INFO_LOG(SLIPPI_ONLINE, "Sending ack packet for frame %d (player %d) to peer at %d:%d", frame, + packetPlayerPort, peer->address.host, peer->address.port); + + ENetPacket* epac = + enet_packet_create(spac.getData(), spac.getDataSize(), ENET_PACKET_FLAG_UNSEQUENCED); + int sendResult = enet_peer_send(peer, 2, epac); } break; @@ -222,28 +288,49 @@ unsigned int SlippiNetplayClient::OnData(sf::Packet& packet) break; } - lastFrameAcked = frame > lastFrameAcked ? frame : lastFrameAcked; + u8 packetPlayerPort; + if (!(packet >> packetPlayerPort)) + { + ERROR_LOG(SLIPPI_ONLINE, "Netplay ack packet too small to read player index"); + break; + } + u8 pIdx = PlayerIdxFromPort(packetPlayerPort); + if (pIdx >= m_remotePlayerCount) + { + ERROR_LOG(SLIPPI_ONLINE, "Got ack packet with invalid player idx %d", pIdx); + break; + } + + INFO_LOG(SLIPPI_ONLINE, "Received ack packet from player %d(%d) [%d]...", packetPlayerPort, + pIdx, frame); + + lastFrameAcked[pIdx] = frame > lastFrameAcked[pIdx] ? frame : lastFrameAcked[pIdx]; // Remove old timings - while (!ackTimers.empty() && ackTimers.front().frame < frame) + while (!ackTimers[pIdx].empty() && ackTimers[pIdx].front().frame < frame) { - ackTimers.pop(); + ackTimers[pIdx].pop(); } // Don't get a ping if we do not have the right ack frame - if (ackTimers.empty() || ackTimers.front().frame != frame) + if (ackTimers[pIdx].empty() || ackTimers[pIdx].front().frame != frame) { break; } - auto sendTime = ackTimers.front().timeUs; - ackTimers.pop(); + auto sendTime = ackTimers[pIdx].front().timeUs; + ackTimers[pIdx].pop(); - pingUs = Common::Timer::GetTimeUs() - sendTime; - if (g_ActiveConfig.bShowNetPlayPing && frame % SLIPPI_PING_DISPLAY_INTERVAL == 0) + pingUs[pIdx] = Common::Timer::GetTimeUs() - sendTime; + if (g_ActiveConfig.bShowNetPlayPing && frame % SLIPPI_PING_DISPLAY_INTERVAL == 0 && pIdx == 0) { - OSD::AddTypedMessage(OSD::MessageType::NetPlayPing, - StringFromFormat("Ping: %u", pingUs / 1000), OSD::Duration::NORMAL, + std::stringstream pingDisplay; + pingDisplay << "Ping: " << (pingUs[0] / 1000); + for (int i = 1; i < m_remotePlayerCount; i++) + { + pingDisplay << " | " << (pingUs[i] / 1000); + } + OSD::AddTypedMessage(OSD::MessageType::NetPlayPing, pingDisplay.str(), OSD::Duration::NORMAL, OSD::Color::CYAN); } } @@ -252,8 +339,10 @@ unsigned int SlippiNetplayClient::OnData(sf::Packet& packet) case NetPlay::NP_MSG_SLIPPI_MATCH_SELECTIONS: { auto s = readSelectionsFromPacket(packet); - INFO_LOG(SLIPPI_ONLINE, "[Netplay] Received selections from opponent"); - matchInfo.remotePlayerSelections.Merge(*s); + INFO_LOG(SLIPPI_ONLINE, "[Netplay] Received selections from opponent with player idx %d", + s->playerIdx); + u8 idx = PlayerIdxFromPort(s->playerIdx); + matchInfo.remotePlayerSelections[idx].Merge(*s); // This might be a good place to reset some logic? Game can't start until we receive this msg // so this should ensure that everything is initialized before the game starts @@ -261,19 +350,17 @@ unsigned int SlippiNetplayClient::OnData(sf::Packet& packet) // Reset remote pad queue such that next inputs that we get are not compared to inputs from last // game - remotePadQueue.clear(); + remotePadQueue[idx].clear(); } break; case NetPlay::NP_MSG_SLIPPI_CHAT_MESSAGE: { auto playerSelection = ReadChatMessageFromPacket(packet); - auto messageId = playerSelection->messageId; - + INFO_LOG(SLIPPI_ONLINE, "[Netplay] Received chat message from opponent %d: %d", + playerSelection->playerIdx, playerSelection->messageId); // set message id to netplay instance - remoteChatMessageId = messageId; - // Show chat message OSD - INFO_LOG(SLIPPI_ONLINE, "[Netplay] Received chat message from opponent %i", messageId); + remoteChatMessageSelection = std::move(playerSelection); } break; @@ -295,21 +382,19 @@ unsigned int SlippiNetplayClient::OnData(sf::Packet& packet) void SlippiNetplayClient::writeToPacket(sf::Packet& packet, SlippiPlayerSelections& s) { - u8 playerIndex = isDecider ? 0 : 1; - u8 teamId = 0; - packet << static_cast(NetPlay::NP_MSG_SLIPPI_MATCH_SELECTIONS); packet << s.characterId << s.characterColor << s.isCharacterSelected; - packet << playerIndex; + packet << s.playerIdx; packet << s.stageId << s.isStageSelected; packet << s.rngOffset; - packet << teamId; + packet << s.teamId; } -void SlippiNetplayClient::WriteChatMessageToPacket(sf::Packet& packet, int messageId) +void SlippiNetplayClient::WriteChatMessageToPacket(sf::Packet& packet, int messageId, u8 playerIdx) { packet << static_cast(NetPlay::NP_MSG_SLIPPI_CHAT_MESSAGE); packet << messageId; + packet << playerIdx; } std::unique_ptr @@ -318,6 +403,7 @@ SlippiNetplayClient::ReadChatMessageFromPacket(sf::Packet& packet) auto s = std::make_unique(); packet >> s->messageId; + packet >> s->playerIdx; return std::move(s); } @@ -327,20 +413,17 @@ SlippiNetplayClient::readSelectionsFromPacket(sf::Packet& packet) { auto s = std::make_unique(); - u8 playerIndex; - u8 teamId; - packet >> s->characterId; packet >> s->characterColor; packet >> s->isCharacterSelected; - packet >> playerIndex; + packet >> s->playerIdx; packet >> s->stageId; packet >> s->isStageSelected; packet >> s->rngOffset; - packet >> teamId; + packet >> s->teamId; return std::move(s); } @@ -350,26 +433,33 @@ void SlippiNetplayClient::Send(sf::Packet& packet) enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE; u8 channelId = 0; - NetPlay::MessageId mid = ((u8*)packet.getData())[0]; - if (mid == NetPlay::NP_MSG_SLIPPI_PAD || mid == NetPlay::NP_MSG_SLIPPI_PAD_ACK) + for (int i = 0; i < m_server.size(); i++) { - // Slippi communications do not need reliable connection and do not need to - // be received in order. Channel is changed so that other reliable communications - // do not block anything. This may not be necessary if order is not maintained? - flags = ENET_PACKET_FLAG_UNSEQUENCED; - channelId = 1; - } + NetPlay::MessageId mid = ((u8*)packet.getData())[0]; + if (mid == NetPlay::NP_MSG_SLIPPI_PAD || mid == NetPlay::NP_MSG_SLIPPI_PAD_ACK) + { + // Slippi communications do not need reliable connection and do not need to + // be received in order. Channel is changed so that other reliable communications + // do not block anything. This may not be necessary if order is not maintained? + flags = ENET_PACKET_FLAG_UNSEQUENCED; + channelId = 1; + } - ENetPacket* epac = enet_packet_create(packet.getData(), packet.getDataSize(), flags); - enet_peer_send(m_server, channelId, epac); + ENetPacket* epac = enet_packet_create(packet.getData(), packet.getDataSize(), flags); + int sendResult = enet_peer_send(m_server[i], channelId, epac); + } } void SlippiNetplayClient::Disconnect() { ENetEvent netEvent; slippiConnectStatus = SlippiConnectStatus::NET_CONNECT_STATUS_DISCONNECTED; - if (m_server) - enet_peer_disconnect(m_server, 0); + if (!m_server.empty()) + for (int i = 0; i < m_server.size(); i++) + { + INFO_LOG(SLIPPI_ONLINE, "[Netplay] Disconnecting peer %d", m_server[i]->address.port); + enet_peer_disconnect(m_server[i], 0); + } else return; @@ -381,15 +471,18 @@ void SlippiNetplayClient::Disconnect() enet_packet_destroy(netEvent.packet); break; case ENET_EVENT_TYPE_DISCONNECT: - m_server = nullptr; - return; + INFO_LOG(SLIPPI_ONLINE, "[Netplay] Got disconnect from peer %d", netEvent.peer->address.port); + break; default: break; } } // didn't disconnect gracefully force disconnect - enet_peer_reset(m_server); - m_server = nullptr; + for (int i = 0; i < m_server.size(); i++) + { + enet_peer_reset(m_server[i]); + } + m_server.clear(); SLIPPI_NETPLAY = nullptr; } @@ -406,83 +499,174 @@ void SlippiNetplayClient::SendAsync(std::unique_ptr packet) void SlippiNetplayClient::ThreadFunc() { // Let client die 1 second before host such that after a swap, the client won't be connected to - int attemptCountLimit = 16; + u64 startTime = Common::Timer::GetTimeMs(); + u64 timeout = 8000; + + std::vector connections; + std::vector remoteAddrs; + for (int i = 0; i < m_remotePlayerCount; i++) + { + remoteAddrs.push_back(m_server[i]->address); + connections.push_back(false); + } - int attemptCount = 0; while (slippiConnectStatus == SlippiConnectStatus::NET_CONNECT_STATUS_INITIATED) { // This will confirm that connection went through successfully ENetEvent netEvent; int net = enet_host_service(m_client, &netEvent, 500); - if (net > 0 && netEvent.type == ENET_EVENT_TYPE_CONNECT) + if (net > 0) { - // TODO: Confirm gecko codes match? - if (netEvent.peer) + switch (netEvent.type) { - WARN_LOG(SLIPPI_ONLINE, "[Netplay] Overwritting server"); - m_server = netEvent.peer; - } + case ENET_EVENT_TYPE_RECEIVE: + if (!netEvent.peer) + { + INFO_LOG(SLIPPI_ONLINE, "[Netplay] got receive event with nil peer"); + continue; + } + INFO_LOG(SLIPPI_ONLINE, "[Netplay] got receive event with peer addr %x:%d", + netEvent.peer->address.host, netEvent.peer->address.port); + break; + case ENET_EVENT_TYPE_DISCONNECT: + if (!netEvent.peer) + { + INFO_LOG(SLIPPI_ONLINE, "[Netplay] got disconnect event with nil peer"); + continue; + } + INFO_LOG(SLIPPI_ONLINE, "[Netplay] got disconnect event with peer addr %x:%d", + netEvent.peer->address.host, netEvent.peer->address.port); + break; + + case ENET_EVENT_TYPE_CONNECT: + { + if (!netEvent.peer) + { + INFO_LOG(SLIPPI_ONLINE, "[Netplay] got connect event with nil peer"); + continue; + } + + INFO_LOG(SLIPPI_ONLINE, "[Netplay] got connect event with peer addr %x:%d", + netEvent.peer->address.host, netEvent.peer->address.port); + for (int i = 0; i < m_server.size(); i++) + { + INFO_LOG(SLIPPI_ONLINE, "[Netplay] Comparing connection address: %x:%d - %x:%d", + remoteAddrs[i].host, remoteAddrs[i].port, netEvent.peer->address.host, + netEvent.peer->address.port); + if (remoteAddrs[i].host == netEvent.peer->address.host && + remoteAddrs[i].port == netEvent.peer->address.port) + { + INFO_LOG(SLIPPI_ONLINE, "[Netplay] Overwriting ENetPeer for address: %x:%d", + netEvent.peer->address.host, netEvent.peer->address.port); + INFO_LOG(SLIPPI_ONLINE, + "[Netplay] Overwriting ENetPeer with id (%d) with new peer of id %d", + m_server[i]->connectID, netEvent.peer->connectID); + m_server[i] = netEvent.peer; + connections[i] = true; + break; + } + } + break; + } + } + } + + bool allConnected = true; + for (int i = 0; i < m_remotePlayerCount; i++) + { + if (!connections[i]) + allConnected = false; + } + + if (allConnected) + { m_client->intercept = ENetUtil::InterceptCallback; - slippiConnectStatus = SlippiConnectStatus::NET_CONNECT_STATUS_CONNECTED; INFO_LOG(SLIPPI_ONLINE, "Slippi online connection successful!"); + slippiConnectStatus = SlippiConnectStatus::NET_CONNECT_STATUS_CONNECTED; break; } + for (int i = 0; i < m_remotePlayerCount; i++) + { + INFO_LOG(SLIPPI_ONLINE, "m_client peer %d state: %d", i, m_client->peers[i].state); + } WARN_LOG_FMT(SLIPPI_ONLINE, "[Netplay] Not yet connected. Res: {}, Type: {}", net, netEvent.type); // Time out after enough time has passed - attemptCount++; - if (attemptCount >= attemptCountLimit || !m_do_loop.IsSet()) + u64 curTime = Common::Timer::GetTimeMs(); + if ((curTime - startTime) >= timeout || !m_do_loop.IsSet()) { + for (int i = 0; i < m_remotePlayerCount; i++) + { + if (!connections[i]) + { + failedConnections.push_back(i); + } + } + slippiConnectStatus = SlippiConnectStatus::NET_CONNECT_STATUS_FAILED; INFO_LOG(SLIPPI_ONLINE, "Slippi online connection failed"); return; } } + INFO_LOG(SLIPPI_ONLINE, "Successfully initialized %d connections", m_server.size()); + for (int i = 0; i < m_server.size(); i++) + { + INFO_LOG(SLIPPI_ONLINE, "Connection %d: %d, %d", i, m_server[i]->address.host, + m_server[i]->address.port); + } + bool qos_success = false; #ifdef _WIN32 QOS_VERSION ver = {1, 0}; - if (Config::Get(Config::NETPLAY_ENABLE_QOS) && QOSCreateHandle(&ver, &m_qos_handle)) + if (QOSCreateHandle(&ver, &m_qos_handle)) { - // from win32.c - struct sockaddr_in sin = {0}; - - sin.sin_family = AF_INET; - sin.sin_port = ENET_HOST_TO_NET_16(m_server->host->address.port); - sin.sin_addr.s_addr = m_server->host->address.host; - - if (QOSAddSocketToFlow(m_qos_handle, m_server->host->socket, reinterpret_cast(&sin), - // this is 0x38 - QOSTrafficTypeControl, QOS_NON_ADAPTIVE_FLOW, &m_qos_flow_id)) + for (int i = 0; i < m_server.size(); i++) { - DWORD dscp = 0x2e; + // from win32.c + struct sockaddr_in sin = {0}; - // this will fail if we're not admin - // sets DSCP to the same as linux (0x2e) - QOSSetFlow(m_qos_handle, m_qos_flow_id, QOSSetOutgoingDSCPValue, sizeof(DWORD), &dscp, 0, - nullptr); + sin.sin_family = AF_INET; + sin.sin_port = ENET_HOST_TO_NET_16(m_server[i]->host->address.port); + sin.sin_addr.s_addr = m_server[i]->host->address.host; - qos_success = true; + if (QOSAddSocketToFlow(m_qos_handle, m_server[i]->host->socket, + reinterpret_cast(&sin), + // this is 0x38 + QOSTrafficTypeControl, QOS_NON_ADAPTIVE_FLOW, &m_qos_flow_id)) + { + DWORD dscp = 0x2e; + + // this will fail if we're not admin + // sets DSCP to the same as linux (0x2e) + QOSSetFlow(m_qos_handle, m_qos_flow_id, QOSSetOutgoingDSCPValue, sizeof(DWORD), &dscp, 0, + nullptr); + + qos_success = true; + } } } #else - if (Config::Get(Config::NETPLAY_ENABLE_QOS)) + if (SConfig::GetInstance().bQoSEnabled) { + for (int i = 0; i < m_server.size(); i++) + { #ifdef __linux__ - // highest priority - int priority = 7; - setsockopt(m_server->host->socket, SOL_SOCKET, SO_PRIORITY, &priority, sizeof(priority)); + // highest priority + int priority = 7; + setsockopt(m_server[i]->host->socket, SOL_SOCKET, SO_PRIORITY, &priority, sizeof(priority)); #endif - // https://www.tucny.com/Home/dscp-tos - // ef is better than cs7 - int tos_val = 0xb8; - qos_success = - setsockopt(m_server->host->socket, IPPROTO_IP, IP_TOS, &tos_val, sizeof(tos_val)) == 0; + // https://www.tucny.com/Home/dscp-tos + // ef is better than cs7 + int tos_val = 0xb8; + qos_success = + setsockopt(m_server[i]->host->socket, IPPROTO_IP, IP_TOS, &tos_val, sizeof(tos_val)) == 0; + } } #endif @@ -499,21 +683,31 @@ void SlippiNetplayClient::ThreadFunc() if (net > 0) { sf::Packet rpac; + bool sameClient = false; switch (netEvent.type) { case ENET_EVENT_TYPE_RECEIVE: rpac.append(netEvent.packet->data, netEvent.packet->dataLength); - OnData(rpac); + OnData(rpac, netEvent.peer); enet_packet_destroy(netEvent.packet); break; case ENET_EVENT_TYPE_DISCONNECT: + for (int i = 0; i < m_remotePlayerCount; i++) + { + if (remoteAddrs[i].host == netEvent.peer->address.host && + remoteAddrs[i].port == netEvent.peer->address.port) + { + sameClient = true; + break; + } + } ERROR_LOG(SLIPPI_ONLINE, "[Netplay] Disconnected Event detected: %s", - netEvent.peer == m_server ? "same client" : "diff client"); + sameClient ? "same client" : "diff client"); // If the disconnect event doesn't come from the client we are actually listening to, // it can be safely ignored - if (netEvent.peer == m_server) + if (sameClient) { m_do_loop.Clear(); // Stop the loop, will trigger a disconnect } @@ -528,7 +722,12 @@ void SlippiNetplayClient::ThreadFunc() if (m_qos_handle != 0) { if (m_qos_flow_id != 0) - QOSRemoveSocketFromFlow(m_qos_handle, m_server->host->socket, m_qos_flow_id, 0); + { + for (int i = 0; i < m_server.size(); i++) + { + QOSRemoveSocketFromFlow(m_qos_handle, m_server[i]->host->socket, m_qos_flow_id, 0); + } + } QOSCloseHandle(m_qos_handle); } #endif @@ -552,37 +751,42 @@ SlippiNetplayClient::SlippiConnectStatus SlippiNetplayClient::GetSlippiConnectSt return slippiConnectStatus; } +std::vector SlippiNetplayClient::GetFailedConnections() +{ + return failedConnections; +} + void SlippiNetplayClient::StartSlippiGame() { // Reset variables to start a new game - lastFrameAcked = 0; - - FrameTiming timing; - timing.frame = 0; - timing.timeUs = Common::Timer::GetTimeUs(); - lastFrameTiming = timing; hasGameStarted = false; localPadQueue.clear(); - // Remote pad should have been cleared when receiving the match selections + for (int i = 0; i < m_remotePlayerCount; i++) + { + FrameTiming timing; + timing.frame = 0; + timing.timeUs = Common::Timer::GetTimeUs(); + lastFrameTiming[i] = timing; + lastFrameAcked[i] = 0; + + // Reset ack timers + std::queue empty; + std::swap(ackTimers[i], empty); + } // Reset match info for next game matchInfo.Reset(); - - // Reset ack timers - ackTimers = {}; } void SlippiNetplayClient::SendConnectionSelected() { isConnectionSelected = true; - auto spac = std::make_unique(); *spac << static_cast(NetPlay::NP_MSG_SLIPPI_CONN_SELECTED); SendAsync(std::move(spac)); } - void SlippiNetplayClient::SendSlippiPad(std::unique_ptr pad) { auto status = slippiConnectStatus; @@ -594,14 +798,12 @@ void SlippiNetplayClient::SendSlippiPad(std::unique_ptr pad) { return; } - // if (pad && isDecider) //{ // ERROR_LOG(SLIPPI_ONLINE, "[%d] %X %X %X %X %X %X %X %X", pad->frame, pad->padBuf[0], // pad->padBuf[1], pad->padBuf[2], pad->padBuf[3], pad->padBuf[4], pad->padBuf[5], // pad->padBuf[6], pad->padBuf[7]); //} - if (pad) { // Add latest local pad report to queue @@ -609,8 +811,20 @@ void SlippiNetplayClient::SendSlippiPad(std::unique_ptr pad) } // Remove pad reports that have been received and acked - while (!localPadQueue.empty() && localPadQueue.back()->frame < lastFrameAcked) + int minAckFrame = lastFrameAcked[0]; + for (int i = 1; i < m_remotePlayerCount; i++) { + if (lastFrameAcked[i] < minAckFrame) + minAckFrame = lastFrameAcked[i]; + } + INFO_LOG(SLIPPI_ONLINE, + "Checking to drop local inputs, oldest frame: %d | minAckFrame: %d | %d, %d, %d", + localPadQueue.back()->frame, minAckFrame, lastFrameAcked[0], lastFrameAcked[1], + lastFrameAcked[2]); + while (!localPadQueue.empty() && localPadQueue.back()->frame < minAckFrame) + { + INFO_LOG(SLIPPI_ONLINE, "Dropping local input for frame %d from queue", + localPadQueue.back()->frame); localPadQueue.pop_back(); } @@ -619,43 +833,45 @@ void SlippiNetplayClient::SendSlippiPad(std::unique_ptr pad) // If pad queue is empty now, there's no reason to send anything return; } - auto frame = localPadQueue.front()->frame; - auto spac = std::make_unique(); *spac << static_cast(NetPlay::NP_MSG_SLIPPI_PAD); *spac << frame; - + *spac << this->playerIdx; // INFO_LOG(SLIPPI_ONLINE, "Sending a packet of inputs [%d]...", frame); for (auto it = localPadQueue.begin(); it != localPadQueue.end(); ++it) { - /*INFO_LOG(SLIPPI_ONLINE, "Send [%d] -> %02X %02X %02X %02X %02X %02X %02X %02X", (*it)->frame, - (*it)->padBuf[0], (*it)->padBuf[1], (*it)->padBuf[2], (*it)->padBuf[3], - (*it)->padBuf[4], (*it)->padBuf[5], (*it)->padBuf[6], (*it)->padBuf[7]);*/ + // INFO_LOG(SLIPPI_ONLINE, "Send [%d] -> %02X %02X %02X %02X %02X %02X %02X %02X", (*it)->frame, + // (*it)->padBuf[0], + // (*it)->padBuf[1], (*it)->padBuf[2], (*it)->padBuf[3], (*it)->padBuf[4], + // (*it)->padBuf[5], + // (*it)->padBuf[6], (*it)->padBuf[7]); spac->append((*it)->padBuf, SLIPPI_PAD_DATA_SIZE); // only transfer 8 bytes per pad } - SendAsync(std::move(spac)); - u64 time = Common::Timer::GetTimeUs(); hasGameStarted = true; - FrameTiming timing; - timing.frame = frame; - timing.timeUs = time; - lastFrameTiming = timing; + for (int i = 0; i < m_remotePlayerCount; i++) + { + FrameTiming timing; + timing.frame = frame; + timing.timeUs = time; + lastFrameTiming[i] = timing; - // Add send time to ack timers - FrameTiming sendTime; - sendTime.frame = frame; - sendTime.timeUs = time; - ackTimers.emplace(sendTime); + // Add send time to ack timers + FrameTiming sendTime; + sendTime.frame = frame; + sendTime.timeUs = time; + ackTimers[i].emplace(sendTime); + } } void SlippiNetplayClient::SetMatchSelections(SlippiPlayerSelections& s) { matchInfo.localPlayerSelections.Merge(s); + matchInfo.localPlayerSelections.playerIdx = playerIdx; // Send packet containing selections auto spac = std::make_unique(); @@ -663,11 +879,26 @@ void SlippiNetplayClient::SetMatchSelections(SlippiPlayerSelections& s) SendAsync(std::move(spac)); } -u8 SlippiNetplayClient::GetSlippiRemoteChatMessage() +SlippiPlayerSelections SlippiNetplayClient::GetSlippiRemoteChatMessage() { - u8 copiedMessageId = remoteChatMessageId; - remoteChatMessageId = 0; // Clear it out - return copiedMessageId; + SlippiPlayerSelections copiedSelection = SlippiPlayerSelections(); + + if (remoteChatMessageSelection != nullptr && SConfig::GetInstance().m_slippiEnableQuickChat) + { + copiedSelection.messageId = remoteChatMessageSelection->messageId; + copiedSelection.playerIdx = remoteChatMessageSelection->playerIdx; + + // Clear it out + remoteChatMessageSelection->messageId = 0; + remoteChatMessageSelection->playerIdx = 0; + } + else + { + copiedSelection.messageId = 0; + copiedSelection.playerIdx = 0; + } + + return copiedSelection; } u8 SlippiNetplayClient::GetSlippiRemoteSentChatMessage() @@ -677,13 +908,14 @@ u8 SlippiNetplayClient::GetSlippiRemoteSentChatMessage() return copiedMessageId; } -std::unique_ptr SlippiNetplayClient::GetSlippiRemotePad(int32_t curFrame) +std::unique_ptr SlippiNetplayClient::GetSlippiRemotePad(int32_t curFrame, + int index) { std::lock_guard lk(pad_mutex); // TODO: Is this the correct lock? std::unique_ptr padOutput = std::make_unique(); - if (remotePadQueue.empty()) + if (remotePadQueue[index].empty()) { auto emptyPad = std::make_unique(0); @@ -695,22 +927,54 @@ std::unique_ptr SlippiNetplayClient::GetSlippiRemotePad(i return std::move(padOutput); } - padOutput->latestFrame = remotePadQueue.front()->frame; - + padOutput->latestFrame = 0; // Copy the entire remaining remote buffer - for (auto it = remotePadQueue.begin(); it != remotePadQueue.end(); ++it) + for (auto it = remotePadQueue[index].begin(); it != remotePadQueue[index].end(); ++it) { + if ((*it)->frame > padOutput->latestFrame) + padOutput->latestFrame = (*it)->frame; + auto padIt = std::begin((*it)->padBuf); padOutput->data.insert(padOutput->data.end(), padIt, padIt + SLIPPI_PAD_FULL_SIZE); } - // Remove pad reports that should no longer be needed - while (remotePadQueue.size() > 1 && remotePadQueue.back()->frame < curFrame) + return std::move(padOutput); +} + +void SlippiNetplayClient::DropOldRemoteInputs(int32_t curFrame) +{ + std::lock_guard lk(pad_mutex); + + // Remove pad reports that should no longer be needed, compute the lowest frame recieved by + // all remote players that can be safely dropped. + int lowestCommonFrame = 0; + for (int i = 0; i < m_remotePlayerCount; i++) { - remotePadQueue.pop_back(); + int playerFrame = 0; + for (auto it = remotePadQueue[i].begin(); it != remotePadQueue[i].end(); ++it) + { + if (it->get()->frame > playerFrame) + playerFrame = it->get()->frame; + } + + if (lowestCommonFrame == 0 || playerFrame < lowestCommonFrame) + lowestCommonFrame = playerFrame; } - return std::move(padOutput); + // INFO_LOG(SLIPPI_ONLINE, "Checking for remotePadQueue inputs to drop, lowest common: %d, [0]: + // %d, [1]: %d, [2]: %d", + // lowestCommonFrame, playerFrame[0], playerFrame[1], playerFrame[2]); + for (int i = 0; i < m_remotePlayerCount; i++) + { + INFO_LOG(SLIPPI_ONLINE, "remotePadQueue[%d] size: %d", i, remotePadQueue[i].size()); + while (remotePadQueue[i].size() > 1 && remotePadQueue[i].back()->frame < lowestCommonFrame && + remotePadQueue[i].back()->frame < curFrame) + { + INFO_LOG(SLIPPI_ONLINE, "Popping inputs for frame %d from back of player %d queue", + remotePadQueue[i].back()->frame, i); + remotePadQueue[i].pop_back(); + } + } } SlippiMatchInfo* SlippiNetplayClient::GetMatchInfo() @@ -718,51 +982,87 @@ SlippiMatchInfo* SlippiNetplayClient::GetMatchInfo() return &matchInfo; } -u64 SlippiNetplayClient::GetSlippiPing() -{ - return pingUs; -} - int32_t SlippiNetplayClient::GetSlippiLatestRemoteFrame() { std::lock_guard lk(pad_mutex); // TODO: Is this the correct lock? - if (remotePadQueue.empty()) + // Return the lowest frame among remote queues + int lowestFrame = 0; + for (int i = 0; i < m_remotePlayerCount; i++) { - return 0; + if (remotePadQueue[i].empty()) + { + return 0; + } + + int f = remotePadQueue[i].front()->frame; + if (f < lowestFrame || lowestFrame == 0) + { + lowestFrame = f; + } } - return remotePadQueue.front()->frame; + return lowestFrame; } +// return the largest time offset among all remote players s32 SlippiNetplayClient::CalcTimeOffsetUs() { - if (frameOffsetData.buf.empty()) + bool empty = true; + for (int i = 0; i < m_remotePlayerCount; i++) + { + if (!frameOffsetData[i].buf.empty()) + { + empty = false; + break; + } + } + if (empty) { return 0; } - std::vector buf; - std::copy(frameOffsetData.buf.begin(), frameOffsetData.buf.end(), std::back_inserter(buf)); - - // TODO: Does this work? - std::sort(buf.begin(), buf.end()); - - int bufSize = (int)buf.size(); - int offset = (int)((1.0f / 3.0f) * bufSize); - int end = bufSize - offset; - - int sum = 0; - for (int i = offset; i < end; i++) + std::vector offsets; + for (int i = 0; i < m_remotePlayerCount; i++) { - sum += buf[i]; + if (frameOffsetData[i].buf.empty()) + continue; + + std::vector buf; + std::copy(frameOffsetData[i].buf.begin(), frameOffsetData[i].buf.end(), + std::back_inserter(buf)); + + // TODO: Does this work? + std::sort(buf.begin(), buf.end()); + + int bufSize = (int)buf.size(); + int offset = (int)((1.0f / 3.0f) * bufSize); + int end = bufSize - offset; + + int sum = 0; + for (int i = offset; i < end; i++) + { + sum += buf[i]; + } + + int count = end - offset; + if (count <= 0) + { + return 0; // What do I return here? + } + + s32 result = sum / count; + offsets.push_back(result); } - int count = end - offset; - if (count <= 0) + s32 maxOffset = offsets.front(); + for (int i = 1; i < offsets.size(); i++) { - return 0; // What do I return here? + if (offsets[i] > maxOffset) + maxOffset = offsets[i]; } - return sum / count; + // INFO_LOG(SLIPPI_ONLINE, "Time offsets, [0]: %d, [1]: %d, [2]: %d", offsets[0], offsets[1], + // offsets[2]); + return maxOffset; } diff --git a/Source/Core/Core/Slippi/SlippiNetplay.h b/Source/Core/Core/Slippi/SlippiNetplay.h index 53bc944083..0c2fc710c0 100644 --- a/Source/Core/Core/Slippi/SlippiNetplay.h +++ b/Source/Core/Core/Slippi/SlippiNetplay.h @@ -13,6 +13,7 @@ #include #include #include +#include #include #include "Common/CommonTypes.h" #include "Common/Event.h" @@ -21,7 +22,6 @@ #include "Core/NetPlayProto.h" #include "Core/Slippi/SlippiPad.h" #include "InputCommon/GCPadStatus.h" - #ifdef _WIN32 #include #endif @@ -29,18 +29,24 @@ #define SLIPPI_ONLINE_LOCKSTEP_INTERVAL \ 30 // Number of frames to wait before attempting to time-sync #define SLIPPI_PING_DISPLAY_INTERVAL 60 +#define SLIPPI_REMOTE_PLAYER_MAX 3 +#define SLIPPI_REMOTE_PLAYER_COUNT 3 struct SlippiRemotePadOutput { int32_t latestFrame; + u8 playerIdx; std::vector data; }; class SlippiPlayerSelections { public: + u8 playerIdx = 0; u8 characterId = 0; u8 characterColor = 0; + u8 teamId = 0; + bool isCharacterSelected = false; u16 stageId = 0; @@ -64,6 +70,7 @@ public: { this->characterId = s.characterId; this->characterColor = s.characterColor; + this->teamId = s.teamId; this->isCharacterSelected = true; } } @@ -73,6 +80,7 @@ public: characterId = 0; characterColor = 0; isCharacterSelected = false; + teamId = 0; stageId = 0; isStageSelected = false; @@ -85,12 +93,15 @@ class SlippiMatchInfo { public: SlippiPlayerSelections localPlayerSelections; - SlippiPlayerSelections remotePlayerSelections; + SlippiPlayerSelections remotePlayerSelections[SLIPPI_REMOTE_PLAYER_MAX]; void Reset() { localPlayerSelections.Reset(); - remotePlayerSelections.Reset(); + for (int i = 0; i < SLIPPI_REMOTE_PLAYER_MAX; i++) + { + remotePlayerSelections[i].Reset(); + } } }; @@ -101,8 +112,9 @@ public: void SendAsync(std::unique_ptr packet); SlippiNetplayClient(bool isDecider); // Make a dummy client - SlippiNetplayClient(const std::string& address, const u16 remotePort, const u16 localPort, - bool isDecider); + SlippiNetplayClient(std::vector addrs, std::vector ports, + const u8 remotePlayerCount, const u16 localPort, bool isDecider, + u8 playerIdx); ~SlippiNetplayClient(); // Slippi Online @@ -117,23 +129,26 @@ public: bool IsDecider(); bool IsConnectionSelected(); + u8 LocalPlayerPort(); SlippiConnectStatus GetSlippiConnectStatus(); + std::vector GetFailedConnections(); void StartSlippiGame(); void SendConnectionSelected(); void SendSlippiPad(std::unique_ptr pad); void SetMatchSelections(SlippiPlayerSelections& s); - std::unique_ptr GetSlippiRemotePad(int32_t curFrame); + std::unique_ptr GetSlippiRemotePad(int32_t curFrame, int index); + void DropOldRemoteInputs(int32_t curFrame); SlippiMatchInfo* GetMatchInfo(); - u64 GetSlippiPing(); - s32 GetSlippiLatestRemoteFrame(); - u8 GetSlippiRemoteChatMessage(); + int32_t GetSlippiLatestRemoteFrame(); + SlippiPlayerSelections GetSlippiRemoteChatMessage(); u8 GetSlippiRemoteSentChatMessage(); s32 CalcTimeOffsetUs(); - void WriteChatMessageToPacket(sf::Packet& packet, int messageId); + void WriteChatMessageToPacket(sf::Packet& packet, int messageId, u8 playerIdx); std::unique_ptr ReadChatMessageFromPacket(sf::Packet& packet); - u8 remoteChatMessageId = 0; // most recent chat message id from opponent + std::unique_ptr remoteChatMessageSelection = + nullptr; // most recent chat message player selection (message + player index) u8 remoteSentChatMessageId = 0; // most recent chat message id that current player sent protected: @@ -148,8 +163,9 @@ protected: std::queue> m_async_queue; ENetHost* m_client = nullptr; - ENetPeer* m_server = nullptr; + std::vector m_server; std::thread m_thread; + u8 m_remotePlayerCount = 0; std::string m_selected_game; Common::Flag m_is_running{false}; @@ -166,23 +182,30 @@ protected: u64 timeUs; }; - struct + struct FrameOffsetData { // TODO: Should the buffer size be dynamic based on time sync interval or not? int idx; std::vector buf; - } frameOffsetData; + }; bool isConnectionSelected = false; bool isDecider = false; - int32_t lastFrameAcked; bool hasGameStarted = false; - FrameTiming lastFrameTiming; - u64 pingUs; - std::deque> localPadQueue; // most recent inputs at start of deque - std::deque> remotePadQueue; // most recent inputs at start of deque - std::queue ackTimers; + u8 playerIdx = 0; + + std::deque> localPadQueue; // most recent inputs at start of deque + std::deque> + remotePadQueue[SLIPPI_REMOTE_PLAYER_MAX]; // most recent inputs at start of deque + + u64 pingUs[SLIPPI_REMOTE_PLAYER_MAX]; + int32_t lastFrameAcked[SLIPPI_REMOTE_PLAYER_MAX]; + FrameOffsetData frameOffsetData[SLIPPI_REMOTE_PLAYER_MAX]; + FrameTiming lastFrameTiming[SLIPPI_REMOTE_PLAYER_MAX]; + std::array, SLIPPI_REMOTE_PLAYER_MAX> ackTimers; + SlippiConnectStatus slippiConnectStatus = SlippiConnectStatus::NET_CONNECT_STATUS_UNSET; + std::vector failedConnections; SlippiMatchInfo matchInfo; bool m_is_recording = false; @@ -191,7 +214,8 @@ protected: std::unique_ptr readSelectionsFromPacket(sf::Packet& packet); private: - unsigned int OnData(sf::Packet& packet); + u8 PlayerIdxFromPort(u8 port); + unsigned int OnData(sf::Packet& packet, ENetPeer* peer); void Send(sf::Packet& packet); void Disconnect(); @@ -207,7 +231,7 @@ private: extern SlippiNetplayClient* SLIPPI_NETPLAY; // singleton static pointer -inline bool IsOnline() +static bool IsOnline() { return SLIPPI_NETPLAY != nullptr; } diff --git a/Source/Core/Core/Slippi/SlippiPad.cpp b/Source/Core/Core/Slippi/SlippiPad.cpp index df0c329227..ecf001e30d 100644 --- a/Source/Core/Core/Slippi/SlippiPad.cpp +++ b/Source/Core/Core/Slippi/SlippiPad.cpp @@ -19,6 +19,14 @@ SlippiPad::SlippiPad(int32_t frame, u8* padBuf) : SlippiPad(frame) memcpy(this->padBuf, padBuf, SLIPPI_PAD_DATA_SIZE); } +SlippiPad::SlippiPad(int32_t frame, u8 playerIdx, u8 *padBuf) : SlippiPad(frame) +{ + this->frame = frame; + this->playerIdx = playerIdx; + // Overwrite the data portion of the pad + memcpy(this->padBuf, padBuf, SLIPPI_PAD_DATA_SIZE); +} + SlippiPad::~SlippiPad() { // Do nothing? diff --git a/Source/Core/Core/Slippi/SlippiPad.h b/Source/Core/Core/Slippi/SlippiPad.h index 8b7d2171a8..b25da60cc4 100644 --- a/Source/Core/Core/Slippi/SlippiPad.h +++ b/Source/Core/Core/Slippi/SlippiPad.h @@ -10,8 +10,10 @@ class SlippiPad public: SlippiPad(int32_t frame); SlippiPad(int32_t frame, u8* padBuf); + SlippiPad(int32_t frame, u8 playerIdx, u8 *padBuf); ~SlippiPad(); int32_t frame; + u8 playerIdx; u8 padBuf[SLIPPI_PAD_FULL_SIZE]; }; diff --git a/Source/Core/Core/Slippi/SlippiUser.cpp b/Source/Core/Core/Slippi/SlippiUser.cpp index 873bf8b57c..278cd89a50 100644 --- a/Source/Core/Core/Slippi/SlippiUser.cpp +++ b/Source/Core/Core/Slippi/SlippiUser.cpp @@ -106,7 +106,7 @@ bool SlippiUser::AttemptLogin() { std::string user_file_path = getUserFilePath(); - INFO_LOG(SLIPPI_ONLINE, "Looking for file at: %s", user_file_path.c_str()); + // INFO_LOG(SLIPPI_ONLINE, "Looking for file at: %s", user_file_path.c_str()); { // Put the filename here in its own scope because we don't really need it elsewhere @@ -312,6 +312,7 @@ SlippiUser::UserInfo SlippiUser::parseFile(std::string file_contents) info.play_key = readString(res, "playKey"); info.connect_code = readString(res, "connectCode"); info.latest_version = readString(res, "latestVersion"); + info.port = res.value("port", -1); return info; } diff --git a/Source/Core/Core/Slippi/SlippiUser.h b/Source/Core/Core/Slippi/SlippiUser.h index 3b7166d567..0da36d7a5b 100644 --- a/Source/Core/Core/Slippi/SlippiUser.h +++ b/Source/Core/Core/Slippi/SlippiUser.h @@ -19,6 +19,8 @@ public: std::string connect_code = ""; std::string latest_version = ""; std::string file_contents = ""; + + int port; }; SlippiUser();