diff --git a/Data/Sys/GameFiles/GALE01/GameSetup.dat b/Data/Sys/GameFiles/GALE01/GameSetup.dat new file mode 100644 index 0000000000..2f6672becd Binary files /dev/null and b/Data/Sys/GameFiles/GALE01/GameSetup.dat differ diff --git a/Data/Sys/GameFiles/GALE01/GameSetup_gui.dat b/Data/Sys/GameFiles/GALE01/GameSetup_gui.dat new file mode 100644 index 0000000000..03344765ae Binary files /dev/null and b/Data/Sys/GameFiles/GALE01/GameSetup_gui.dat differ diff --git a/Data/Sys/GameFiles/GALE01/MxDb.dat b/Data/Sys/GameFiles/GALE01/MxDb.dat new file mode 100644 index 0000000000..0598993b50 Binary files /dev/null and b/Data/Sys/GameFiles/GALE01/MxDb.dat differ diff --git a/Data/Sys/GameFiles/GALE01/MxScn.dat b/Data/Sys/GameFiles/GALE01/MxScn.dat new file mode 100644 index 0000000000..a63207d0a6 Binary files /dev/null and b/Data/Sys/GameFiles/GALE01/MxScn.dat differ diff --git a/Data/Sys/GameFiles/GALE01/SlippiCSS.dat b/Data/Sys/GameFiles/GALE01/SlippiCSS.dat new file mode 100644 index 0000000000..2409f7cac0 Binary files /dev/null and b/Data/Sys/GameFiles/GALE01/SlippiCSS.dat differ diff --git a/Data/Sys/GameFiles/GALE01/slpCSS.dat b/Data/Sys/GameFiles/GALE01/slpCSS.dat index 138ef2ad02..f2c8638ea5 100644 Binary files a/Data/Sys/GameFiles/GALE01/slpCSS.dat and b/Data/Sys/GameFiles/GALE01/slpCSS.dat differ diff --git a/Source/Core/Common/StringUtil.cpp b/Source/Core/Common/StringUtil.cpp index 434523b6f2..bc782a0ca7 100644 --- a/Source/Core/Common/StringUtil.cpp +++ b/Source/Core/Common/StringUtil.cpp @@ -19,8 +19,8 @@ #include #include #include -#include #include +#include #include @@ -441,17 +441,20 @@ void StringPopBackIf(std::string* s, char c) // SLIPPITODO: look into boost locale maybe void ConvertNarrowSpecialSHIFTJIS(std::string& input) { - // Melee doesn't correctly display special characters in narrow form We need to convert them to wide form. - // I couldn't find a library to do this so for now let's just do it manually + // Melee doesn't correctly display special characters in narrow form We need to convert them to + // wide form. I couldn't find a library to do this so for now let's just do it manually static std::unordered_map specialCharConvert = { - {'!', (char16_t)0x8149}, {'"', (char16_t)0x8168}, {'#', (char16_t)0x8194}, {'$', (char16_t)0x8190}, - {'%', (char16_t)0x8193}, {'&', (char16_t)0x8195}, {'\'', (char16_t)0x8166}, {'(', (char16_t)0x8169}, - {')', (char16_t)0x816a}, {'*', (char16_t)0x8196}, {'+', (char16_t)0x817b}, {',', (char16_t)0x8143}, - {'-', (char16_t)0x817c}, {'.', (char16_t)0x8144}, {'/', (char16_t)0x815e}, {':', (char16_t)0x8146}, - {';', (char16_t)0x8147}, {'<', (char16_t)0x8183}, {'=', (char16_t)0x8181}, {'>', (char16_t)0x8184}, - {'?', (char16_t)0x8148}, {'@', (char16_t)0x8197}, {'[', (char16_t)0x816d}, {'\\', (char16_t)0x815f}, - {']', (char16_t)0x816e}, {'^', (char16_t)0x814f}, {'_', (char16_t)0x8151}, {'`', (char16_t)0x814d}, - {'{', (char16_t)0x816f}, {'|', (char16_t)0x8162}, {'}', (char16_t)0x8170}, {'~', (char16_t)0x8160}, + {'!', (char16_t)0x8149}, {'"', (char16_t)0x8168}, {'#', (char16_t)0x8194}, + {'$', (char16_t)0x8190}, {'%', (char16_t)0x8193}, {'&', (char16_t)0x8195}, + {'\'', (char16_t)0x8166}, {'(', (char16_t)0x8169}, {')', (char16_t)0x816a}, + {'*', (char16_t)0x8196}, {'+', (char16_t)0x817b}, {',', (char16_t)0x8143}, + {'-', (char16_t)0x817c}, {'.', (char16_t)0x8144}, {'/', (char16_t)0x815e}, + {':', (char16_t)0x8146}, {';', (char16_t)0x8147}, {'<', (char16_t)0x8183}, + {'=', (char16_t)0x8181}, {'>', (char16_t)0x8184}, {'?', (char16_t)0x8148}, + {'@', (char16_t)0x8197}, {'[', (char16_t)0x816d}, {'\\', (char16_t)0x815f}, + {']', (char16_t)0x816e}, {'^', (char16_t)0x814f}, {'_', (char16_t)0x8151}, + {'`', (char16_t)0x814d}, {'{', (char16_t)0x816f}, {'|', (char16_t)0x8162}, + {'}', (char16_t)0x8170}, {'~', (char16_t)0x8160}, }; int pos = 0; @@ -476,13 +479,13 @@ void ConvertNarrowSpecialSHIFTJIS(std::string& input) input.erase(pos, 1); // Add new chars to pos to replace - auto newChars = (char*)& specialCharConvert[c]; + auto newChars = (char*)&specialCharConvert[c]; input.insert(input.begin() + pos, 1, newChars[0]); input.insert(input.begin() + pos, 1, newChars[1]); } } -std::string ConvertStringForGame(const std::string& input, int length) +std::string TruncateLengthChar(const std::string& input, int length) { auto utf32 = UTF8ToUTF32(input); @@ -492,7 +495,12 @@ std::string ConvertStringForGame(const std::string& input, int length) utf32.resize(length); } - auto utf8 = UTF32toUTF8(utf32); + return UTF32toUTF8(utf32); +} + +std::string ConvertStringForGame(const std::string& input, int length) +{ + auto utf8 = TruncateLengthChar(input, length); auto shiftJis = UTF8ToSHIFTJIS(utf8); ConvertNarrowSpecialSHIFTJIS(shiftJis); diff --git a/Source/Core/Common/StringUtil.h b/Source/Core/Common/StringUtil.h index 7dd79418d0..2389367663 100644 --- a/Source/Core/Common/StringUtil.h +++ b/Source/Core/Common/StringUtil.h @@ -178,6 +178,7 @@ std::u32string UTF8ToUTF32(const std::string& input); std::u32string UTF8ToUTF32(const std::string& input); #endif std::string UTF32toUTF8(const std::u32string& input); +std::string TruncateLengthChar(const std::string& input, int length); std::string ConvertStringForGame(const std::string& input, int length); std::string CP1252ToUTF8(std::string_view str); std::string SHIFTJISToUTF8(std::string_view str); diff --git a/Source/Core/Core/Config/MainSettings.cpp b/Source/Core/Core/Config/MainSettings.cpp index 7efd061fdc..38e5e57e19 100644 --- a/Source/Core/Core/Config/MainSettings.cpp +++ b/Source/Core/Core/Config/MainSettings.cpp @@ -175,12 +175,21 @@ const Info& GetInfoForSIDevice(int channel) const Info& GetInfoForAdapterRumble(int channel) { +#ifndef IS_PLAYBACK static const std::array, 4> infos{ Info{{System::Main, "Core", "AdapterRumble0"}, true}, Info{{System::Main, "Core", "AdapterRumble1"}, true}, Info{{System::Main, "Core", "AdapterRumble2"}, true}, Info{{System::Main, "Core", "AdapterRumble3"}, true}, }; +#else + static const std::array, 4> infos{ + Info{{System::Main, "Core", "AdapterRumble0"}, false}, + Info{{System::Main, "Core", "AdapterRumble1"}, false}, + Info{{System::Main, "Core", "AdapterRumble2"}, false}, + Info{{System::Main, "Core", "AdapterRumble3"}, false}, + }; +#endif return infos[channel]; } diff --git a/Source/Core/Core/GeckoCode.cpp b/Source/Core/Core/GeckoCode.cpp index 2cea74253a..aa82fdff8c 100644 --- a/Source/Core/Core/GeckoCode.cpp +++ b/Source/Core/Core/GeckoCode.cpp @@ -170,6 +170,13 @@ static Installation InstallCodeHandlerLocked(const Core::CPUThreadGuard& guard) if (SConfig::GetSlippiConfig().melee_version == Melee::Version::NTSC || SConfig::GetSlippiConfig().melee_version == Melee::Version::MEX) { + // Here we are replacing a line in the codehandler with a blr. + // The reason for this is that this is the section of the codehandler + // that attempts to read/write commands for the USB Gecko. These calls + // were sometimes interfering with the Slippi EXI calls and causing + // the game to loop infinitely in EXISync. + PowerPC::HostWrite_U32(guard, 0x4E800020, 0x80001D6C); + // Write GCT loader into memory which will eventually load the real GCT into the heap std::string bootloaderData; std::string _bootloaderFilename = File::GetSysDirectory() + GCT_BOOTLOADER; @@ -218,7 +225,7 @@ static Installation InstallCodeHandlerLocked(const Core::CPUThreadGuard& guard) "not write: \"{}\". Need {} bytes, only {} remain.", active_code.name, active_code.codes.size() * CODE_SIZE, end_address - next_address); - continue; + return Installation::Failed; } for (const GeckoCode::Code& code : active_code.codes) diff --git a/Source/Core/Core/HW/EXI/EXI_DeviceSlippi.cpp b/Source/Core/Core/HW/EXI/EXI_DeviceSlippi.cpp index c294279976..0f69f4284e 100644 --- a/Source/Core/Core/HW/EXI/EXI_DeviceSlippi.cpp +++ b/Source/Core/Core/HW/EXI/EXI_DeviceSlippi.cpp @@ -255,6 +255,17 @@ CEXISlippi::~CEXISlippi() SlippiSpectateServer::getInstance().endGame(true); + // Try to determine whether we were playing an in-progress ranked match, if so + // indicate to server that this client has abandoned. Anyone trying to modify + // this behavior to game their rating is subject to get banned. + auto active_match_id = matchmaking->GetMatchmakeResult().id; + if (activeMatchId.find("mode.ranked") != std::string::npos) + { + ERROR_LOG(SLIPPI_ONLINE, "Exit during in-progress ranked game: %s", activeMatchId.c_str()); + gameReporter->ReportAbandonment(activeMatchId); + } + handleConnectionCleanup(); + localSelections.Reset(); g_playbackStatus->resetPlayback(); @@ -1538,7 +1549,10 @@ void CEXISlippi::handleOnlineInputs(u8* payload) m_read_queue.clear(); s32 frame = Common::swap32(&payload[0]); - s32 finalizedFrame = Common::swap32(&payload[4]); + s32 finalized_frame = Common::swap32(&payload[4]); + u32 finalized_frame_checksum = Common::swap32(&payload[8]); + u8 delay = payload[12]; + u8* inputs = &payload[13]; if (frame == 1) { @@ -1565,7 +1579,7 @@ void CEXISlippi::handleOnlineInputs(u8* payload) fallBehindCounter = 0; fallFarBehindCounter = 0; - // Reset character selections as they are no longer needed + // Reset character selections such that they are cleared for next game localSelections.Reset(); if (slippi_netplay) slippi_netplay->StartSlippiGame(); @@ -1578,9 +1592,9 @@ void CEXISlippi::handleOnlineInputs(u8* payload) } // Drop inputs that we no longer need (inputs older than the finalized frame passed in) - slippi_netplay->DropOldRemoteInputs(finalizedFrame); + slippi_netplay->DropOldRemoteInputs(finalized_frame); - bool shouldSkip = shouldSkipOnlineFrame(frame, finalizedFrame); + bool shouldSkip = shouldSkipOnlineFrame(frame, finalized_frame); if (shouldSkip) { // Send inputs that have not yet been acked @@ -1589,7 +1603,7 @@ void CEXISlippi::handleOnlineInputs(u8* payload) else { // Send the input for this frame along with everything that has yet to be acked - handleSendInputs(payload); + handleSendInputs(frame, delay, finalized_frame, finalized_frame_checksum, inputs); } prepareOpponentInputs(frame, shouldSkip); @@ -1764,8 +1778,9 @@ bool CEXISlippi::shouldAdvanceOnlineFrame(s32 frame) // don't understand atm. OSD::AddTypedMessage( OSD::MessageType::PerformanceWarning, - "Your computer is running slow and is impacting the performance of the match.", 10000, - OSD::Color::RED); + "Possible poor match performance detected.\nIf this message appears with most opponents, " + "your computer or network is likely impacting match performance for the other players.", + 10000, OSD::Color::RED); } if (offsetUs < -t2 && !isCurrentlyAdvancing) @@ -1800,14 +1815,11 @@ bool CEXISlippi::shouldAdvanceOnlineFrame(s32 frame) return false; } -void CEXISlippi::handleSendInputs(u8* payload) +void CEXISlippi::handleSendInputs(s32 frame, u8 delay, s32 checksum_frame, u32 checksum, u8* inputs) { if (isConnectionStalled) return; - s32 frame = Common::swap32(&payload[0]); - u8 delay = payload[8]; - // On the first frame sent, we need to queue up empty dummy pads for as many // frames as we have delay if (frame == 1) @@ -1819,7 +1831,7 @@ void CEXISlippi::handleSendInputs(u8* payload) } } - auto pad = std::make_unique(frame + delay, &payload[9]); + auto pad = std::make_unique(frame + delay, checksum_frame, checksum, inputs); slippi_netplay->SendSlippiPad(std::move(pad)); } @@ -1854,6 +1866,24 @@ void CEXISlippi::prepareOpponentInputs(s32 frame, bool shouldSkip) m_read_queue.push_back(remotePlayerCount); // Indicate the number of remote players std::unique_ptr results[SLIPPI_REMOTE_PLAYER_MAX]; + + for (int i = 0; i < remotePlayerCount; i++) + { + results[i] = slippi_netplay->GetSlippiRemotePad(i, ROLLBACK_MAX_FRAMES); + // results[i] = slippi_netplay->GetFakePadOutput(frame); + + // INFO_LOG(SLIPPI_ONLINE, "Sending checksum values: [%d] %08x", results[i]->checksumFrame, + // results[i]->checksum); + appendWordToBuffer(&m_read_queue, static_cast(results[i]->checksumFrame)); + appendWordToBuffer(&m_read_queue, results[i]->checksum); + } + for (int i = remotePlayerCount; i < SLIPPI_REMOTE_PLAYER_MAX; i++) + { + // Send dummy data for unused players + appendWordToBuffer(&m_read_queue, 0); + appendWordToBuffer(&m_read_queue, 0); + } + int offset[SLIPPI_REMOTE_PLAYER_MAX]; int32_t latestFrameRead[SLIPPI_REMOTE_PLAYER_MAX]{}; @@ -1893,7 +1923,7 @@ void CEXISlippi::prepareOpponentInputs(s32 frame, bool shouldSkip) std::vector tx; // Get pad data if this remote player exists - if (i < remotePlayerCount) + if (i < remotePlayerCount && offset[i] < results[i]->data.size()) { auto txStart = results[i]->data.begin() + offset[i]; auto txEnd = results[i]->data.end(); @@ -2185,12 +2215,71 @@ void CEXISlippi::handleNameEntryLoad(u8* payload) appendWordToBuffer(&m_read_queue, curIndex); } +// teamId 0 = red, 1 = blue, 2 = green +int CEXISlippi::getCharColor(u8 charId, u8 teamId) +{ + switch (charId) + { + case 0x0: // Falcon + switch (teamId) + { + case 0: + return 2; + case 1: + return 5; + case 2: + return 4; + } + case 0x2: // Fox + switch (teamId) + { + case 0: + return 1; + case 1: + return 2; + case 2: + return 3; + } + case 0xC: // Peach + switch (teamId) + { + case 0: + return 0; + case 1: + return 3; + case 2: + return 4; + } + case 0x13: // Sheik + switch (teamId) + { + case 0: + return 1; + case 1: + return 2; + case 2: + return 3; + } + case 0x14: // Falco + switch (teamId) + { + case 0: + return 1; + case 1: + return 2; + case 2: + return 3; + } + } + return 0; +} + void CEXISlippi::prepareOnlineMatchState() { // 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, + 0x32, 0x01, 0x86, 0x4C, 0xC3, 0x00, 0x00, 0x00, 0x01, 0x01, 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, @@ -2247,10 +2336,11 @@ void CEXISlippi::prepareOnlineMatchState() slippi_netplay = matchmaking->GetNetplayClient(); #endif - // This happens on the initial connection to a player. Let's now grab the allowed stages - // returned to us from the matchmaking service and pick a new random stage before sending - // the selections to the opponent - allowedStages = matchmaking->GetStages(); + // This happens on the initial connection to a player. The matchmaking object is ephemeral, it + // gets re-created when a connection is terminated, that said, it can still be useful to know + // who we were connected to after they disconnect from us, for example in the case of + // reporting a match. So let's copy the results. + allowedStages = recentMmResult.stages; // Clear stage pool so that when we call getRandomStage it will use full list stagePool.clear(); localSelections.stageId = getRandomStage(); @@ -2305,16 +2395,7 @@ void CEXISlippi::prepareOnlineMatchState() // Here we are connected, check to see if we should init play session if (!is_play_session_active) { - std::vector uids; - - auto mmPlayers = matchmaking->GetPlayerInfo(); - for (auto mmp : mmPlayers) - { - uids.push_back(mmp.uid); - } - - game_reporter->StartNewSession(uids); - + game_reporter->StartNewSession(); is_play_session_active = true; } } @@ -2415,10 +2496,7 @@ void CEXISlippi::prepareOnlineMatchState() rps[i].isCharacterSelected = true; } - if (lastSearch.mode == SlippiMatchmaking::OnlinePlayMode::TEAMS) - { - remotePlayerCount = 3; - } + remotePlayerCount = lastSearch.mode == SlippiMatchmaking::OnlinePlayMode::TEAMS ? 3 : 1; oppName = std::string("Player"); #endif @@ -2433,25 +2511,39 @@ void CEXISlippi::prepareOnlineMatchState() remoteCharOk = false; } - // TODO: This is annoying, ideally remotePlayerSelections would just include everyone including + // TODO: Ideally remotePlayerSelections would just include everyone including the local player // the local player // TODO: Would also simplify some logic in the Netplay class - std::vector orderedSelections(4); - orderedSelections[lps.playerIdx] = lps; + // Here we are storing pointers to the player selections. That means that we can technically + // modify the values from here, which is probably not the cleanest thing since they're coming + // from the netplay class. Unfortunately, I think it might be required for the overwrite stuff + // to work correctly though, maybe on a tiebreak in ranked? + std::vector orderedSelections(remotePlayerCount + 1); + orderedSelections[lps.playerIdx] = &lps; for (int i = 0; i < remotePlayerCount; i++) { - orderedSelections[rps[i].playerIdx] = rps[i]; + orderedSelections[rps[i].playerIdx] = &rps[i]; + } + + // Overwrite selections + for (int i = 0; i < overwrite_selections.size(); i++) + { + const auto& ow = overwrite_selections[i]; + + orderedSelections[i]->characterId = ow.characterId; + orderedSelections[i]->characterColor = ow.characterColor; + orderedSelections[i]->stageId = ow.stageId; } // Overwrite stage information. Make sure everyone loads the same stage u16 stageId = 0x1F; // Default to battlefield if there was no selection for (const auto& selections : orderedSelections) { - if (!selections.isStageSelected) + if (!selections->isStageSelected) continue; // Stage selected by this player, use that selection - stageId = selections.stageId; + stageId = selections->stageId; break; } @@ -2505,11 +2597,11 @@ void CEXISlippi::prepareOnlineMatchState() INFO_LOG_FMT(SLIPPI_ONLINE, "Rng Offset: {:#x}", rngOffset); // Check if everyone is the same color - auto color = orderedSelections[0].teamId; + auto color = orderedSelections[0]->teamId; bool areAllSameTeam = true; for (const auto& s : orderedSelections) { - if (s.teamId != color) + if (s->teamId != color) areAllSameTeam = false; } @@ -2526,21 +2618,21 @@ void CEXISlippi::prepareOnlineMatchState() // Overwrite player character choices for (auto& s : orderedSelections) { - if (!s.isCharacterSelected) + if (!s->isCharacterSelected) { continue; } if (areAllSameTeam) { // Overwrite teamId. Color is overwritten by ASM - s.teamId = teamAssignments[s.playerIdx]; + s->teamId = teamAssignments[s->playerIdx]; } // Overwrite player character - onlineMatchBlock[0x60 + (s.playerIdx) * 0x24] = s.characterId; - onlineMatchBlock[0x63 + (s.playerIdx) * 0x24] = s.characterColor; - onlineMatchBlock[0x67 + (s.playerIdx) * 0x24] = 0; - onlineMatchBlock[0x69 + (s.playerIdx) * 0x24] = s.teamId; + onlineMatchBlock[0x60 + (s->playerIdx) * 0x24] = s->characterId; + onlineMatchBlock[0x63 + (s->playerIdx) * 0x24] = s->characterColor; + onlineMatchBlock[0x67 + (s->playerIdx) * 0x24] = 0; + onlineMatchBlock[0x69 + (s->playerIdx) * 0x24] = s->teamId; } // Handle Singles/Teams specific logic @@ -2573,8 +2665,7 @@ void CEXISlippi::prepareOnlineMatchState() *stage = Common::swap16(stageId); // Turn pause off in unranked/ranked, on in other modes - auto pauseAllowed = !SlippiMatchmaking::IsFixedRulesMode(lastSearch.mode) && - lastSearch.mode != SlippiMatchmaking::OnlinePlayMode::TEAMS; + auto pauseAllowed = lastSearch.mode == SlippiMatchmaking::OnlinePlayMode::DIRECT; u8* gameBitField3 = static_cast(&onlineMatchBlock[2]); *gameBitField3 = pauseAllowed ? *gameBitField3 & 0xF7 : *gameBitField3 | 0x8; //*gameBitField3 = *gameBitField3 | 0x8; @@ -2633,24 +2724,35 @@ void CEXISlippi::prepareOnlineMatchState() } // Create the opponent string using the names of all players on opposing teams - int teamIdx = onlineMatchBlock[0x69 + m_local_player_index * 0x24]; - std::string oppText = ""; - for (int i = 0; i < 4; i++) + std::vector opponentNames = {}; + if (matchmaking->RemotePlayerCount() == 1) { - if (i == m_local_player_index) - continue; - - if (onlineMatchBlock[0x69 + i * 0x24] != teamIdx) + opponentNames.push_back(matchmaking->GetPlayerName(remotePlayerIndex)); + } + else + { + int teamIdx = onlineMatchBlock[0x69 + localPlayerIndex * 0x24]; + for (int i = 0; i < 4; i++) { - if (oppText != "") - oppText += "/"; + if (localPlayerIndex == i || onlineMatchBlock[0x69 + i * 0x24] == teamIdx) + continue; - oppText += matchmaking->GetPlayerName(i); + opponentNames.push_back(matchmaking->GetPlayerName(i)); } } - if (matchmaking->RemotePlayerCount() == 1) - oppText = matchmaking->GetPlayerName(m_remote_player_index); - oppName = ConvertStringForGame(oppText, MAX_NAME_LENGTH * 2 + 1); + + auto numOpponents = opponentNames.size() == 0 ? 1 : opponentNames.size(); + auto charsPerName = (MAX_NAME_LENGTH - (numOpponents - 1)) / numOpponents; + std::string oppText = ""; + for (auto& name : opponentNames) + { + if (oppText != "") + oppText += "/"; + + oppText += TruncateLengthChar(name, charsPerName); + } + + oppName = ConvertStringForGame(oppText, MAX_NAME_LENGTH); m_read_queue.insert(m_read_queue.end(), oppName.begin(), oppName.end()); #ifdef LOCAL_TESTING @@ -3003,6 +3105,9 @@ void CEXISlippi::handleConnectionCleanup() // Reset any forced errors forcedError.clear(); + // Reset any selection overwrites + overwrite_selections.clear(); + // Reset play session is_play_session_active = false; @@ -3022,29 +3127,50 @@ void CEXISlippi::prepareNewSeed() appendWordToBuffer(&m_read_queue, newSeed); } -void CEXISlippi::handleReportGame(u8* payload) +void CEXISlippi::handleReportGame(const SlippiExiTypes::ReportGameQuery& query) { -#ifndef LOCAL_TESTING SlippiGameReporter::GameReport r; - r.duration_frames = Common::swap32(&payload[0]); + r.match_id = recentMmResult.id; + r.mode = static_cast(query.mode); + r.duration_frames = query.frame_length; + r.game_index = query.game_index; + r.tiebreak_index = query.tiebreak_index; + r.winner_idx = query.winner_idx; + r.stage_id = Common::FromBigEndian(*(u16*)&query.game_info_block[0xE]); + r.game_end_method = query.game_end_method; + r.lras_initiator = query.lras_initiator; - // ERROR_LOG_FMT(SLIPPI_ONLINE, "Frames: {}", r.duration_frames); + ERROR_LOG_FMT(SLIPPI_ONLINE, + "Mode: {} / {}, Frames: {}, GameIdx: {}, TiebreakIdx: {}, WinnerIdx: {}, " + "StageId: {}, GameEndMethod: {}, LRASInitiator: {}", + r.mode, query.mode, r.duration_frames, r.game_index, r.tiebreak_index, r.winner_idx, + r.stage_id, r.game_end_method, r.lras_initiator); - for (auto i = 0; i < 2; ++i) + auto mm_players = recentMmResult.players; + + for (auto i = 0; i < 4; ++i) { SlippiGameReporter::PlayerReport p; - auto offset = i * 6; - p.stocks_remaining = payload[5 + offset]; + p.uid = mm_players.size() > i ? mm_players[i].uid : ""; + p.slot_type = query.players[i].slot_type; + p.stocks_remaining = query.players[i].stocks_remaining; + p.damage_done = query.players[i].damage_done; + p.char_id = query.game_info_block[0x60 + 0x24 * i]; + p.color_id = query.game_info_block[0x63 + 0x24 * i]; + p.starting_stocks = query.game_info_block[0x62 + 0x24 * i]; + p.starting_percent = Common::FromBigEndian(*(u16*)&query.game_info_block[0x70 + 0x24 * i]); - auto swappedDamageDone = Common::swap32(&payload[6 + offset]); - p.damage_done = *(float*)&swappedDamageDone; - - // ERROR_LOG_FMT(SLIPPI_ONLINE, "Stocks: {}, DamageDone: %f", p.stocks_remaining, - // p.damage_done); + ERROR_LOG_FMT(SLIPPI_ONLINE, + "UID: {}, Port Type: {}, Stocks: {}, DamageDone: {}, CharId: {}, ColorId: {}, " + "StartStocks: {}, " + "StartPercent: {}", + p.uid, p.slot_type, p.stocks_remaining, p.damage_done, p.char_id, p.color_id, + p.starting_stocks, p.starting_percent); r.players.push_back(p); } +#ifndef LOCAL_TESTING game_reporter->StartReport(r); #endif } @@ -3063,6 +3189,84 @@ void CEXISlippi::prepareDelayResponse() m_read_queue.push_back(Config::Get(Config::SLIPPI_ONLINE_DELAY)); } +void CEXISlippi::handleOverwriteSelections(const SlippiExiTypes::OverwriteSelectionsQuery& query) +{ + overwrite_selections.clear(); + + for (int i = 0; i < 4; i++) + { + // TODO: I'm pretty sure this contine would cause bugs if we tried to overwrite only player 1 + // TODO: and not player 0. Right now though GamePrep always overwrites both p0 and p1 so it's + // fine + // TODO: The bug would likely happen in the prepareOnlineMatchState, it would overwrite the + // TODO: wrong players I think + if (!query.chars[i].is_set) + continue; + + SlippiPlayerSelections s; + s.isCharacterSelected = true; + s.characterId = query.chars[i].char_id; + s.characterColor = query.chars[i].char_color_id; + s.isStageSelected = true; + s.stageId = query.stage_id; + s.playerIdx = i; + + overwrite_selections.push_back(s); + } +} + +void CEXISlippi::handleGamePrepStepComplete(const SlippiExiTypes::GpCompleteStepQuery& query) +{ + if (!slippi_netplay) + return; + + SlippiGamePrepStepResults res; + res.step_idx = query.step_idx; + res.char_selection = query.char_selection; + res.char_color_selection = query.char_color_selection; + memcpy(res.stage_selections, query.stage_selections, 2); + + slippi_netplay->SendGamePrepStep(res); +} + +void CEXISlippi::prepareGamePrepOppStep(const SlippiExiTypes::GpFetchStepQuery& query) +{ + SlippiExiTypes::GpFetchStepResponse resp; + + m_read_queue.clear(); + + // Start by indicating not found + resp.is_found = false; + +#ifdef LOCAL_TESTING + static int delay_count = 0; + + delay_count++; + if (delay_count >= 90) + { + resp.is_found = true; + resp.is_skip = true; // Will make client just pick the next available options + + delay_count = 0; + } +#else + SlippiGamePrepStepResults res; + if (slippi_netplay && slippi_netplay->GetGamePrepResults(query.step_idx, res)) + { + // If we have received a response from the opponent, prepare the values for response + resp.is_found = true; + resp.is_skip = false; + resp.char_selection = res.char_selection; + resp.char_color_selection = res.char_color_selection; + memcpy(resp.stage_selections, res.stage_selections, 2); + } +#endif + + auto data_ptr = (u8*)&resp; + m_read_queue.insert(m_read_queue.end(), data_ptr, + data_ptr + sizeof(SlippiExiTypes::GpFetchStepResponse)); +} + void CEXISlippi::DMAWrite(u32 _uAddr, u32 _uSize) { auto& system = Core::System::GetInstance(); @@ -3106,13 +3310,16 @@ void CEXISlippi::DMAWrite(u32 _uAddr, u32 _uSize) memPtr[bufLoc], memPtr[bufLoc + 1], memPtr[bufLoc + 2], memPtr[bufLoc + 3], memPtr[bufLoc + 4]); + u8 prevCommandByte = 0; + while (bufLoc < _uSize) { byte = memPtr[bufLoc]; if (!payloadSizes.count(byte)) { // This should never happen. Do something else if it does? - WARN_LOG_FMT(EXPANSIONINTERFACE, "EXI SLIPPI: Invalid command byte: {:#x}", byte); + ERROR_LOG_FMT(SLIPPI, "EXI SLIPPI: Invalid command byte: {:#x}. Prev command: {:#x}", byte, + prevCommandByte); return; } @@ -3204,7 +3411,7 @@ void CEXISlippi::DMAWrite(u32 _uAddr, u32 _uSize) prepareNewSeed(); break; case CMD_REPORT_GAME: - handleReportGame(&memPtr[bufLoc + 1]); + handleReportGame(SlippiExiTypes::Convert(&memPtr[bufLoc])); break; case CMD_GCT_LENGTH: prepareGctLength(); @@ -3215,12 +3422,25 @@ void CEXISlippi::DMAWrite(u32 _uAddr, u32 _uSize) case CMD_GET_DELAY: prepareDelayResponse(); break; + case CMD_OVERWRITE_SELECTIONS: + handleOverwriteSelections( + SlippiExiTypes::Convert(&memPtr[bufLoc])); + break; + case CMD_GP_FETCH_STEP: + prepareGamePrepOppStep( + SlippiExiTypes::Convert(&memPtr[bufLoc])); + break; + case CMD_GP_COMPLETE_STEP: + handleGamePrepStepComplete( + SlippiExiTypes::Convert(&memPtr[bufLoc])); + break; default: writeToFileAsync(&memPtr[bufLoc], payloadLen + 1, ""); SlippiSpectateServer::getInstance().write(&memPtr[bufLoc], payloadLen + 1); break; } + prevCommandByte = byte; bufLoc += payloadLen + 1; } } diff --git a/Source/Core/Core/HW/EXI/EXI_DeviceSlippi.h b/Source/Core/Core/HW/EXI/EXI_DeviceSlippi.h index 69d58de3dd..df3cf697c9 100644 --- a/Source/Core/Core/HW/EXI/EXI_DeviceSlippi.h +++ b/Source/Core/Core/HW/EXI/EXI_DeviceSlippi.h @@ -8,6 +8,7 @@ #include "Common/FileUtil.h" #include "Common/IOFile.h" #include "Core/Slippi/SlippiDirectCodes.h" +#include "Core/Slippi/SlippiExiTypes.h" #include "Core/Slippi/SlippiGame.h" #include "Core/Slippi/SlippiGameFileLoader.h" #include "Core/Slippi/SlippiGameReporter.h" @@ -75,6 +76,9 @@ private: CMD_GET_NEW_SEED = 0xBC, CMD_REPORT_GAME = 0xBD, CMD_FETCH_CODE_SUGGESTION = 0xBE, + CMD_OVERWRITE_SELECTIONS = 0xBF, + CMD_GP_COMPLETE_STEP = 0xC0, + CMD_GP_FETCH_STEP = 0xC1, // Misc CMD_LOG_MESSAGE = 0xD0, @@ -102,8 +106,8 @@ private: {CMD_RECEIVE_COMMANDS, 1}, // The following are all commands used to play back a replay and - // have fixed sizes - {CMD_PREPARE_REPLAY, 0}, + // have fixed sizes unless otherwise specified + {CMD_PREPARE_REPLAY, 0xFFFF}, // Variable size... will only work if by itself {CMD_READ_FRAME, 4}, {CMD_IS_STOCK_STEAL, 5}, {CMD_GET_LOCATION, 6}, @@ -111,7 +115,7 @@ private: {CMD_GET_GECKO_CODES, 0}, // The following are used for Slippi online and also have fixed sizes - {CMD_ONLINE_INPUTS, 21}, + {CMD_ONLINE_INPUTS, 25}, {CMD_CAPTURE_SAVESTATE, 32}, {CMD_LOAD_SAVESTATE, 32}, {CMD_GET_MATCH_STATE, 0}, @@ -124,8 +128,12 @@ private: {CMD_GET_ONLINE_STATUS, 0}, {CMD_CLEANUP_CONNECTION, 0}, {CMD_GET_NEW_SEED, 0}, - {CMD_REPORT_GAME, 16}, + {CMD_REPORT_GAME, static_cast(sizeof(SlippiExiTypes::ReportGameQuery) - 1)}, {CMD_FETCH_CODE_SUGGESTION, 31}, + {CMD_OVERWRITE_SELECTIONS, + static_cast(sizeof(SlippiExiTypes::OverwriteSelectionsQuery) - 1)}, + {CMD_GP_COMPLETE_STEP, static_cast(sizeof(SlippiExiTypes::GpCompleteStepQuery) - 1)}, + {CMD_GP_FETCH_STEP, static_cast(sizeof(SlippiExiTypes::GpFetchStepQuery) - 1)}, // Misc {CMD_LOG_MESSAGE, 0xFFFF}, // Variable size... will only work if by itself @@ -173,7 +181,7 @@ private: bool isDisconnected(); void handleOnlineInputs(u8* payload); void prepareOpponentInputs(s32 frame, bool shouldSkip); - void handleSendInputs(u8* payload); + void handleSendInputs(s32 frame, u8 delay, s32 checksum_frame, u32 checksum, u8* inputs); void handleCaptureSavestate(u8* payload); void handleLoadSavestate(u8* payload); void handleNameEntryLoad(u8* payload); @@ -187,7 +195,10 @@ private: void prepareOnlineStatus(); void handleConnectionCleanup(); void prepareNewSeed(); - void handleReportGame(u8* payload); + void handleReportGame(const SlippiExiTypes::ReportGameQuery& query); + void handleOverwriteSelections(const SlippiExiTypes::OverwriteSelectionsQuery& query); + void handleGamePrepStepComplete(const SlippiExiTypes::GpCompleteStepQuery& query); + void prepareGamePrepOppStep(const SlippiExiTypes::GpFetchStepQuery& query); // replay playback stuff void prepareGameInfo(u8* payload); @@ -231,9 +242,13 @@ private: std::unique_ptr m_current_game = nullptr; SlippiSpectateServer* m_slippiserver = nullptr; SlippiMatchmaking::MatchSearchSettings lastSearch; + SlippiMatchmaking::MatchmakeResult recentMmResult; std::vector stagePool; + // Used by ranked to set game prep selections + std::vector overwrite_selections; + u32 frameSeqIdx = 0; bool isEnetInitialized = false; diff --git a/Source/Core/Core/HW/VideoInterface.cpp b/Source/Core/Core/HW/VideoInterface.cpp index b2e0694bf0..d6890469a3 100644 --- a/Source/Core/Core/HW/VideoInterface.cpp +++ b/Source/Core/Core/HW/VideoInterface.cpp @@ -849,7 +849,7 @@ void VideoInterfaceManager::Update(u64 ticks) // Try calling SI Poll every time update is called SerialInterface::UpdateDevices(); Core::UpdateInputGate(!Config::Get(Config::MAIN_INPUT_BACKGROUND_INPUT), - Config::Get(Config::MAIN_LOCK_CURSOR)); + Config::Get(Config::MAIN_LOCK_CURSOR)); // Movie's frame counter should be updated before actually rendering the frame, // in case frame counter display is enabled @@ -884,7 +884,8 @@ void VideoInterfaceManager::Update(u64 ticks) if (m_half_line_count == 0 || m_half_line_count == GetHalfLinesPerEvenField()) Core::Callback_NewField(m_system); - // SLIPPINOTES: this section is disable because we would rather poll every chance we get to reduce lag + // SLIPPINOTES: this section is disable because we would rather poll every chance we get to reduce + // lag // // If an SI poll is scheduled to happen on this half-line, do it! // if (m_half_line_of_next_si_poll == m_half_line_count) diff --git a/Source/Core/Core/NetPlayProto.h b/Source/Core/Core/NetPlayProto.h index 669cba0d2e..07ad5b95f2 100644 --- a/Source/Core/Core/NetPlayProto.h +++ b/Source/Core/Core/NetPlayProto.h @@ -157,6 +157,7 @@ enum class MessageID : u8 SLIPPI_MATCH_SELECTIONS = 0x82, SLIPPI_CONN_SELECTED = 0x83, SLIPPI_CHAT_MESSAGE = 0x84, + SLIPPI_COMPLETE_STEP = 0x85, GolfRequest = 0x90, GolfSwitch = 0x91, diff --git a/Source/Core/Core/Slippi/SlippiExiTypes.h b/Source/Core/Core/Slippi/SlippiExiTypes.h new file mode 100644 index 0000000000..613383ee15 --- /dev/null +++ b/Source/Core/Core/Slippi/SlippiExiTypes.h @@ -0,0 +1,107 @@ +#pragma once + +#include "Common/CommonTypes.h" + +#define REPORT_PLAYER_COUNT 4 + +namespace SlippiExiTypes +{ +// Using pragma pack here will remove any structure padding which is what EXI comms expect +// https://www.geeksforgeeks.org/how-to-avoid-structure-padding-in-c/ +// Note that none of these classes should be used outside of the handler function, pragma pack +// is supposedly not very efficient? +#pragma pack(1) + +struct ReportGameQueryPlayer +{ + u8 slot_type; + u8 stocks_remaining; + float damage_done; +}; + +struct ReportGameQuery +{ + u8 command; + u8 mode; + u32 frame_length; + u32 game_index; + u32 tiebreak_index; + s8 winner_idx; + u8 game_end_method; + s8 lras_initiator; + ReportGameQueryPlayer players[REPORT_PLAYER_COUNT]; + u8 game_info_block[312]; +}; + +struct GpCompleteStepQuery +{ + u8 command; + u8 step_idx; + u8 char_selection; + u8 char_color_selection; + u8 stage_selections[2]; +}; + +struct GpFetchStepQuery +{ + u8 command; + u8 step_idx; +}; + +struct GpFetchStepResponse +{ + u8 is_found; + u8 is_skip; + u8 char_selection; + u8 char_color_selection; + u8 stage_selections[2]; +}; + +struct OverwriteCharSelections +{ + u8 is_set; + u8 char_id; + u8 char_color_id; +}; +struct OverwriteSelectionsQuery +{ + u8 command; + u16 stage_id; + OverwriteCharSelections chars[4]; +}; + +// Not sure if resetting is strictly needed, might be contained to the file +#pragma pack() + +template +inline T Convert(u8* payload) +{ + return *reinterpret_cast(payload); +} + +// Here we define custom convert functions for any type that larger than u8 sized fields to convert +// from big-endian + +template <> +inline ReportGameQuery Convert(u8* payload) +{ + auto q = *reinterpret_cast(payload); + q.frameLength = Common::FromBigEndian(q.frameLength); + q.gameIndex = Common::FromBigEndian(q.gameIndex); + q.tiebreakIndex = Common::FromBigEndian(q.tiebreakIndex); + for (int i = 0; i < REPORT_PLAYER_COUNT; i++) + { + auto* p = &q.players[i]; + p->damageDone = Common::FromBigEndian(p->damageDone); + } + return q; +} + +template <> +inline OverwriteSelectionsQuery Convert(u8* payload) +{ + auto q = *reinterpret_cast(payload); + q.stage_id = Common::FromBigEndian(q.stage_id); + return q; +} +}; // namespace SlippiExiTypes diff --git a/Source/Core/Core/Slippi/SlippiGameReporter.cpp b/Source/Core/Core/Slippi/SlippiGameReporter.cpp index 883d404064..6b2eeda410 100644 --- a/Source/Core/Core/Slippi/SlippiGameReporter.cpp +++ b/Source/Core/Core/Slippi/SlippiGameReporter.cpp @@ -9,6 +9,7 @@ #include "Common/Common.h" #include "Core/ConfigManager.h" +#include "Core/Slippi/SlippiMatchmaking.h" #include #include @@ -39,15 +40,15 @@ SlippiGameReporter::SlippiGameReporter(SlippiUser* user) m_user = user; run_thread = true; - reportingThread = std::thread(&SlippiGameReporter::ReportThreadHandler, this); + reporting_thread = std::thread(&SlippiGameReporter::ReportThreadHandler, this); } SlippiGameReporter::~SlippiGameReporter() { run_thread = false; cv.notify_one(); - if (reportingThread.joinable()) - reportingThread.join(); + if (reporting_thread.joinable()) + reporting_thread.join(); if (m_curl) { @@ -62,10 +63,9 @@ void SlippiGameReporter::StartReport(GameReport report) cv.notify_one(); } -void SlippiGameReporter::StartNewSession(std::vector new_player_uids) +void SlippiGameReporter::StartNewSession() { - this->m_player_uids = new_player_uids; - gameIndex = 1; + game_index = 1; } void SlippiGameReporter::ReportThreadHandler() @@ -83,22 +83,45 @@ void SlippiGameReporter::ReportThreadHandler() auto report = game_report_queue.front(); game_report_queue.pop(); - auto userInfo = m_user->GetUserInfo(); + auto ranked = SlippiMatchmaking::OnlinePlayMode::RANKED; + auto unranked = SlippiMatchmaking::OnlinePlayMode::UNRANKED; + bool should_report = report.mode == ranked || report.mode == unranked; + if (!should_report) + { + break; + } + + auto user_info = m_user->GetUserInfo(); + WARN_LOG_FMT(SLIPPI_ONLINE, "Checking game report for game {}. Length: {}...", game_index, + report.duration_frames); // Prepare report json request; - request["uid"] = userInfo.uid; - request["playKey"] = userInfo.play_key; - request["gameIndex"] = gameIndex; + request["matchId"] = report.match_id; + request["uid"] = user_info.uid; + request["playKey"] = user_info.play_key; + request["mode"] = report.mode; + request["gameIndex"] = report.mode == ranked ? report.game_index : game_index; + request["tiebreakIndex"] = report.mode == ranked ? report.tiebreak_index : 0; + request["gameIndex"] = game_index; request["gameDurationFrames"] = report.duration_frames; + request["winnerIdx"] = report.winner_idx; + request["gameEndMethod"] = report.game_end_method; + request["lrasInitiator"] = report.lras_initiator; + request["stageId"] = report.stage_id; json players = json::array(); for (int i = 0; i < report.players.size(); i++) { json p; p["uid"] = m_player_uids[i]; - p["damage_done"] = report.players[i].damage_done; - p["stocks_remaining"] = report.players[i].stocks_remaining; + p["slotType"] = report.players[i].slot_type; + p["damageDone"] = report.players[i].damage_done; + p["stocksRemaining"] = report.players[i].stocks_remaining; + p["characterId"] = report.players[i].char_id; + p["colorId"] = report.players[i].color_id; + p["startingStocks"] = report.players[i].starting_stocks; + p["startingPercent"] = report.players[i].starting_percent; players[i] = p; } @@ -120,8 +143,34 @@ void SlippiGameReporter::ReportThreadHandler() static_cast(res)); } - gameIndex++; + game_index++; Common::SleepCurrentThread(0); } } } + +void SlippiGameReporter::ReportAbandonment(std::string match_id) +{ + auto userInfo = m_user->GetUserInfo(); + + // Prepare report + json request; + request["matchId"] = match_id; + request["uid"] = userInfo.uid; + request["playKey"] = userInfo.play_key; + + auto requestString = request.dump(); + + // Send report + curl_easy_setopt(m_curl, CURLOPT_POST, true); + curl_easy_setopt(m_curl, CURLOPT_URL, ABANDON_URL.c_str()); + curl_easy_setopt(m_curl, CURLOPT_POSTFIELDS, requestString.c_str()); + curl_easy_setopt(m_curl, CURLOPT_POSTFIELDSIZE, requestString.length()); + CURLcode res = curl_easy_perform(m_curl); + + if (res != 0) + { + ERROR_LOG_FMT(SLIPPI_ONLINE, + "[GameReport] Got error executing abandonment request. Err code: {}", res); + } +} diff --git a/Source/Core/Core/Slippi/SlippiGameReporter.h b/Source/Core/Core/Slippi/SlippiGameReporter.h index ea68933e23..ca542d51cc 100644 --- a/Source/Core/Core/Slippi/SlippiGameReporter.h +++ b/Source/Core/Core/Slippi/SlippiGameReporter.h @@ -9,6 +9,7 @@ #include #include #include "Common/CommonTypes.h" +#include "Core/Slippi/SlippiMatchmaking.h" #include "Core/Slippi/SlippiUser.h" class SlippiGameReporter @@ -16,12 +17,27 @@ class SlippiGameReporter public: struct PlayerReport { + std::string uid; + u8 slot_type; float damage_done; u8 stocks_remaining; + u8 char_id; + u8 color_id; + int starting_stocks; + int starting_percent; }; + struct GameReport { + SlippiMatchmaking::OnlinePlayMode mode = SlippiMatchmaking::OnlinePlayMode::UNRANKED; + std::string match_id; u32 duration_frames = 0; + u32 game_index = 0; + u32 tiebreak_index = 0; + s8 winner_idx = 0; + u8 game_end_method = 0; + s8 lras_initiator = 0; + int stage_id; std::vector players; }; @@ -29,20 +45,22 @@ public: ~SlippiGameReporter(); void StartReport(GameReport report); - void StartNewSession(std::vector player_uids); + void ReportAbandonment(std::string match_id); + void StartNewSession(); void ReportThreadHandler(); protected: const std::string REPORT_URL = "https://rankings-dot-slippi.uc.r.appspot.com/report"; + const std::string ABANDON_URL = "https://rankings-dot-slippi.uc.r.appspot.com/abandon"; CURL* m_curl = nullptr; struct curl_slist* m_curl_header_list = nullptr; - u32 gameIndex = 1; + u32 game_index = 1; std::vector m_player_uids; SlippiUser* m_user; std::queue game_report_queue; - std::thread reportingThread; + std::thread reporting_thread; std::mutex mtx; std::condition_variable cv; std::atomic run_thread; diff --git a/Source/Core/Core/Slippi/SlippiMatchmaking.cpp b/Source/Core/Core/Slippi/SlippiMatchmaking.cpp index fbcfeaedc4..f83b18d403 100644 --- a/Source/Core/Core/Slippi/SlippiMatchmaking.cpp +++ b/Source/Core/Core/Slippi/SlippiMatchmaking.cpp @@ -44,6 +44,7 @@ SlippiMatchmaking::SlippiMatchmaking(SlippiUser* user) SlippiMatchmaking::~SlippiMatchmaking() { + is_mm_terminated = true; m_state = ProcessState::ERROR_ENCOUNTERED; m_errorMsg = "Matchmaking shut down"; @@ -148,6 +149,11 @@ void SlippiMatchmaking::MatchmakeThread() { while (IsSearching()) { + if (is_mm_terminated) + { + break; + } + switch (m_state) { case ProcessState::INITIALIZING: @@ -380,7 +386,10 @@ void SlippiMatchmaking::startMatchmaking() // Send message to server to create ticket json request; request["type"] = MmMessageType::CREATE_TICKET; - request["user"] = {{"uid", userInfo.uid}, {"playKey", userInfo.play_key}}; + request["user"] = {{"uid", userInfo.uid}, + {"playKey", userInfo.play_key}, + {"connectCode", userInfo.connect_code}, + {"displayName", userInfo.display_name}}; request["search"] = {{"mode", m_searchSettings.mode}, {"connectCode", connectCodeBuf}}; request["appVersion"] = Common::GetSemVerStr(); request["ipAddressLan"] = lan_addr; @@ -477,6 +486,12 @@ void SlippiMatchmaking::handleMatchmaking() m_remoteIps.clear(); m_playerInfo.clear(); + std::string matchId = getResp.value("matchId", ""); + WARN_LOG_FMT(SLIPPI_ONLINE, "Match ID: {}", matchId); + + std::string match_id = getResp.value("matchId", ""); + WARN_LOG_FMT(SLIPPI_ONLINE, "Match ID: {}", match_id); + auto queue = getResp["players"]; if (queue.is_array()) { @@ -564,6 +579,10 @@ void SlippiMatchmaking::handleMatchmaking() } } + m_mm_result.id = match_id; + m_mm_result.players = m_playerInfo; + m_mm_result.stages = m_allowedStages; + // Disconnect and destroy enet client to mm server terminateMmConnection(); @@ -596,6 +615,11 @@ std::string SlippiMatchmaking::GetPlayerName(u8 port) return m_playerInfo[port].display_name; } +SlippiMatchmaking::MatchmakeResult SlippiMatchmaking::GetMatchmakeResult() +{ + return m_mm_result; +} + u8 SlippiMatchmaking::RemotePlayerCount() { if (m_playerInfo.size() == 0) diff --git a/Source/Core/Core/Slippi/SlippiMatchmaking.h b/Source/Core/Core/Slippi/SlippiMatchmaking.h index 9e225f7c1a..3d07457575 100644 --- a/Source/Core/Core/Slippi/SlippiMatchmaking.h +++ b/Source/Core/Core/Slippi/SlippiMatchmaking.h @@ -47,6 +47,13 @@ public: std::string connectCode = ""; }; + struct MatchmakeResult + { + std::string id = ""; + std::vector players; + std::vector stages; + }; + void FindMatch(MatchSearchSettings settings); void MatchmakeThread(); ProcessState GetMatchmakeState(); @@ -58,6 +65,7 @@ public: std::string GetPlayerName(u8 port); std::vector GetStages(); u8 RemotePlayerCount(); + MatchmakeResult GetMatchmakeResult(); static bool IsFixedRulesMode(OnlinePlayMode mode); protected: @@ -73,6 +81,7 @@ protected: std::default_random_engine generator; bool isMmConnected = false; + bool is_mm_terminated = false; std::thread m_matchmakeThread; @@ -88,6 +97,7 @@ protected: int m_hostPort; int m_localPlayerIndex; std::vector m_remoteIps; + MatchmakeResult m_mm_result; std::vector m_playerInfo; std::vector m_allowedStages; bool m_joinedLobby; diff --git a/Source/Core/Core/Slippi/SlippiNetplay.cpp b/Source/Core/Core/Slippi/SlippiNetplay.cpp index be069f0a2d..50164877a1 100644 --- a/Source/Core/Core/Slippi/SlippiNetplay.cpp +++ b/Source/Core/Core/Slippi/SlippiNetplay.cpp @@ -188,7 +188,10 @@ unsigned int SlippiNetplayClient::OnData(sf::Packet& packet, ENetPeer* peer) // Fetch current time immediately for the most accurate timing calculations u64 curTime = Common::Timer::NowUs(); - int32_t frame; + s32 frame; + s32 checksum_frame; + u32 checksum; + if (!(packet >> frame)) { ERROR_LOG_FMT(SLIPPI_ONLINE, "Netplay packet too small to read frame count"); @@ -201,6 +204,16 @@ unsigned int SlippiNetplayClient::OnData(sf::Packet& packet, ENetPeer* peer) ERROR_LOG_FMT(SLIPPI_ONLINE, "Netplay packet too small to read player index"); break; } + if (!(packet >> checksum_frame)) + { + ERROR_LOG_FMT(SLIPPI_ONLINE, "Netplay packet too small to read checksum frame"); + break; + } + if (!(packet >> checksum)) + { + ERROR_LOG_FMT(SLIPPI_ONLINE, "Netplay packet too small to read checksum value"); + break; + } u8 pIdx = PlayerIdxFromPort(packetPlayerPort); if (pIdx >= m_remotePlayerCount) { @@ -208,6 +221,9 @@ unsigned int SlippiNetplayClient::OnData(sf::Packet& packet, ENetPeer* peer) break; } + // This is the amount of bytes from the start of the packet where the pad data starts + int pad_data_offset = 14; + // This fetches the m_server index that stores the connection we want to overwrite (if // necessary). Note that this index is not necessarily the same as the pIdx because if we have // users connecting with the same WAN, the m_server indices might not match @@ -277,6 +293,7 @@ unsigned int SlippiNetplayClient::OnData(sf::Packet& packet, ENetPeer* peer) frameOffsetData[pIdx].idx = (frameOffsetData[pIdx].idx + 1) % SLIPPI_ONLINE_LOCKSTEP_INTERVAL; + s64 inputs_to_copy; { std::lock_guard lk(pad_mutex); // TODO: Is this the correct lock? @@ -289,45 +306,56 @@ unsigned int SlippiNetplayClient::OnData(sf::Packet& packet, ENetPeer* peer) s64 frame64 = static_cast(frame); s32 headFrame = remotePadQueue[pIdx].empty() ? 0 : remotePadQueue[pIdx].front()->frame; - s64 inputsToCopy = frame64 - static_cast(headFrame); + s64 inputs_to_copy = frame64 - static_cast(headFrame); // Check that the packet actually contains the data it claims to - if ((6 + inputsToCopy * SLIPPI_PAD_DATA_SIZE) > static_cast(packet.getDataSize())) + if ((pad_data_offset + inputs_to_copy * SLIPPI_PAD_DATA_SIZE) > + static_cast(packet.getDataSize())) { ERROR_LOG_FMT( SLIPPI_ONLINE, "Netplay packet too small to read pad buffer. Size: {}, Inputs: {}, MinSize: {}", - static_cast(packet.getDataSize()), inputsToCopy, - 6 + inputsToCopy * SLIPPI_PAD_DATA_SIZE); + static_cast(packet.getDataSize()), inputs_to_copy, + pad_data_offset + inputs_to_copy * SLIPPI_PAD_DATA_SIZE); break; } // Not sure what the max is here. If we never ack frames it could get big... - if (inputsToCopy > 128) + if (inputs_to_copy > 128) { - ERROR_LOG_FMT(SLIPPI_ONLINE, "Netplay packet contained too many frames: {}", inputsToCopy); + ERROR_LOG_FMT(SLIPPI_ONLINE, "Netplay packet contained too many frames: {}", + inputs_to_copy); break; } - for (s64 i = inputsToCopy - 1; i >= 0; i--) + for (s64 i = inputs_to_copy - 1; i >= 0; i--) { - auto pad = std::make_unique(static_cast(frame64 - i), pIdx, - &packetData[6 + i * SLIPPI_PAD_DATA_SIZE]); + auto pad = + std::make_unique(static_cast(frame64 - i), pIdx, + &packetData[pad_data_offset + i * SLIPPI_PAD_DATA_SIZE]); remotePadQueue[pIdx].push_front(std::move(pad)); } } - // Send Ack - sf::Packet spac; - spac << static_cast(NetPlay::MessageID::SLIPPI_PAD_ACK); - spac << frame; - spac << m_player_idx; - // INFO_LOG_FMT(SLIPPI_ONLINE, "Sending ack packet for frame {} (player {}) to peer at {}:{}", - // frame, packetPlayerPort, peer->address.host, peer->address.port); + // Only ack if inputsToCopy is greater than 0. Otherwise we are receiving an old input and + // we should have already acked something in the future. This can also happen in the case + // where a new game starts quickly before the remote queue is reset and if we ack the early + // inputs we will never receive them + if (inputs_to_copy > 0) + { + // Send Ack + sf::Packet spac; + spac << static_cast(NetPlay::MessageID::SLIPPI_PAD_ACK); + spac << frame; + spac << m_player_idx; + // 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); - enet_peer_send(peer, 2, epac); + ENetPacket* epac = + enet_packet_create(spac.getData(), spac.getDataSize(), ENET_PACKET_FLAG_UNSEQUENCED); + int sendResult = enet_peer_send(peer, 2, epac); + } } break; @@ -440,6 +468,20 @@ unsigned int SlippiNetplayClient::OnData(sf::Packet& packet, ENetPeer* peer) } break; + case NetPlay::MessageID::SLIPPI_COMPLETE_STEP: + { + SlippiGamePrepStepResults results; + + packet >> results.step_idx; + packet >> results.char_selection; + packet >> results.char_color_selection; + packet >> results.stage_selections[0]; + packet >> results.stage_selections[1]; + + game_prep_step_queue.push_back(results); + } + break; + default: WARN_LOG_FMT(SLIPPI_ONLINE, "Unknown message received with id : {}", static_cast(mid)); break; @@ -989,6 +1031,9 @@ void SlippiNetplayClient::StartSlippiGame() std::swap(ackTimers[i], empty); } + // Clear game prep queue in case anything is still lingering + game_prep_step_queue.clear(); + // Reset match info for next game matchInfo.Reset(); } @@ -1046,9 +1091,11 @@ void SlippiNetplayClient::SendSlippiPad(std::unique_ptr pad) *spac << static_cast(NetPlay::MessageID::SLIPPI_PAD); *spac << frame; *spac << this->m_player_idx; + *spac << localPadQueue.front()->checksum_frame; + *spac << localPadQueue.front()->checksum; for (auto it = localPadQueue.begin(); it != localPadQueue.end(); ++it) - spac->append((*it)->padBuf, SLIPPI_PAD_DATA_SIZE); // only transfer 8 bytes per pad + spac->append((*it)->pad_buf, SLIPPI_PAD_DATA_SIZE); // only transfer 8 bytes per pad SendAsync(std::move(spac)); u64 time = Common::Timer::NowUs(); @@ -1081,6 +1128,35 @@ void SlippiNetplayClient::SetMatchSelections(SlippiPlayerSelections& s) SendAsync(std::move(spac)); } +void SlippiNetplayClient::SendGamePrepStep(SlippiGamePrepStepResults& s) +{ + auto spac = std::make_unique(); + *spac << static_cast(NetPlay::MessageID::SLIPPI_COMPLETE_STEP); + *spac << s.step_idx; + *spac << s.char_selection; + *spac << s.char_color_selection; + *spac << s.stage_selections[0] << s.stage_selections[1]; + SendAsync(std::move(spac)); +} + +bool SlippiNetplayClient::GetGamePrepResults(u8 step_idx, SlippiGamePrepStepResults& res) +{ + // Just pull stuff off until we find something for the right step. I think that should be fine + while (!game_prep_step_queue.empty()) + { + auto front = game_prep_step_queue.front(); + if (front.step_idx == step_idx) + { + res = front; + return true; + } + + game_prep_step_queue.pop_front(); + } + + return false; +} + SlippiPlayerSelections SlippiNetplayClient::GetSlippiRemoteChatMessage(bool isChatEnabled) { SlippiPlayerSelections copiedSelection = SlippiPlayerSelections(); @@ -1098,6 +1174,7 @@ SlippiPlayerSelections SlippiNetplayClient::GetSlippiRemoteChatMessage(bool isCh { copiedSelection.messageId = 0; copiedSelection.playerIdx = 0; + // if chat is not enabled, automatically send back a message saying so. if (remoteChatMessageSelection != nullptr && !isChatEnabled && (remoteChatMessageSelection->messageId > 0 && @@ -1137,12 +1214,12 @@ std::unique_ptr SlippiNetplayClient::GetFakePadOutput(int if (frame % 60 < 5) { // Return old inputs for a bit - padOutput->latestFrame = frame - (frame % 60); + padOutput->latest_frame = frame - (frame % 60); padOutput->data.insert(padOutput->data.begin(), SLIPPI_PAD_FULL_SIZE, 0); } else if (frame % 60 == 5) { - padOutput->latestFrame = frame; + padOutput->latest_frame = frame; // Add 5 frames of 0'd inputs padOutput->data.insert(padOutput->data.begin(), 5 * SLIPPI_PAD_FULL_SIZE, 0); @@ -1151,7 +1228,7 @@ std::unique_ptr SlippiNetplayClient::GetFakePadOutput(int } else { - padOutput->latestFrame = frame; + padOutput->latest_frame = frame; padOutput->data.insert(padOutput->data.begin(), SLIPPI_PAD_FULL_SIZE, 0); } @@ -1169,9 +1246,9 @@ std::unique_ptr SlippiNetplayClient::GetSlippiRemotePad(i { auto emptyPad = std::make_unique(0); - padOutput->latestFrame = emptyPad->frame; + padOutput->latest_frame = emptyPad->frame; - auto emptyIt = std::begin(emptyPad->padBuf); + auto emptyIt = std::begin(emptyPad->pad_buf); padOutput->data.insert(padOutput->data.end(), emptyIt, emptyIt + SLIPPI_PAD_FULL_SIZE); return std::move(padOutput); @@ -1179,7 +1256,9 @@ std::unique_ptr SlippiNetplayClient::GetSlippiRemotePad(i int inputCount = 0; - padOutput->latestFrame = 0; + padOutput->latest_frame = 0; + padOutput->checksum_frame = remoteChecksum[index].frame; + padOutput->checksum = remoteChecksum[index].value; // Copy inputs from the remote pad queue to the output. We iterate backwards because // we want to get the oldest frames possible (will have been cleared to contain the last @@ -1188,14 +1267,14 @@ std::unique_ptr SlippiNetplayClient::GetSlippiRemotePad(i // game actually needed. for (auto it = remotePadQueue[index].rbegin(); it != remotePadQueue[index].rend(); ++it) { - if ((*it)->frame > padOutput->latestFrame) - padOutput->latestFrame = (*it)->frame; + if ((*it)->frame > padOutput->latest_frame) + padOutput->latest_frame = (*it)->frame; // NOTICE_LOG(SLIPPI_ONLINE, "[%d] (Remote) P%d %08X %08X %08X", (*it)->frame, // index >= playerIdx ? index + 1 : index, Common::swap32(&(*it)->padBuf[0]), // Common::swap32(&(*it)->padBuf[4]), Common::swap32(&(*it)->padBuf[8])); - auto padIt = std::begin((*it)->padBuf); + auto padIt = std::begin((*it)->pad_buf); padOutput->data.insert(padOutput->data.begin(), padIt, padIt + SLIPPI_PAD_FULL_SIZE); // Limit max amount of inputs to send @@ -1231,7 +1310,7 @@ int32_t SlippiNetplayClient::GetSlippiLatestRemoteFrame(int maxFrameCount) for (int i = 0; i < m_remotePlayerCount; i++) { auto rp = GetSlippiRemotePad(i, maxFrameCount); - int f = rp->latestFrame; + int f = rp->latest_frame; if (f < lowestFrame || !isFrameSet) { lowestFrame = f; diff --git a/Source/Core/Core/Slippi/SlippiNetplay.h b/Source/Core/Core/Slippi/SlippiNetplay.h index 852b1e6492..bd61527373 100644 --- a/Source/Core/Core/Slippi/SlippiNetplay.h +++ b/Source/Core/Core/Slippi/SlippiNetplay.h @@ -34,11 +34,21 @@ struct SlippiRemotePadOutput { - int32_t latestFrame{}; + int32_t latest_frame{}; + s32 checksum_frame; + u32 checksum; u8 playerIdx{}; std::vector data; }; +struct SlippiGamePrepStepResults +{ + u8 step_idx; + u8 char_selection; + u8 char_color_selection; + u8 stage_selections[2]; +}; + class SlippiPlayerSelections { public: @@ -90,6 +100,12 @@ public: } }; +struct ChecksumEntry +{ + s32 frame; + u32 value; +}; + class SlippiMatchInfo { public: @@ -137,6 +153,8 @@ public: void SendConnectionSelected(); void SendSlippiPad(std::unique_ptr pad); void SetMatchSelections(SlippiPlayerSelections& s); + void SendGamePrepStep(SlippiGamePrepStepResults& s); + bool GetGamePrepResults(u8 stepIdx, SlippiGamePrepStepResults& res); std::unique_ptr GetFakePadOutput(int frame); std::unique_ptr GetSlippiRemotePad(int index, int maxFrameCount); void DropOldRemoteInputs(int32_t finalizedFrame); @@ -201,7 +219,8 @@ protected: std::deque> localPadQueue; // most recent inputs at start of deque std::deque> remotePadQueue[SLIPPI_REMOTE_PLAYER_MAX]; // most recent inputs at start of deque - + ChecksumEntry remoteChecksum[SLIPPI_REMOTE_PLAYER_MAX]; + std::deque game_prep_step_queue; u64 pingUs[SLIPPI_REMOTE_PLAYER_MAX]; int32_t lastFrameAcked[SLIPPI_REMOTE_PLAYER_MAX]; FrameOffsetData frameOffsetData[SLIPPI_REMOTE_PLAYER_MAX]; diff --git a/Source/Core/Core/Slippi/SlippiPad.cpp b/Source/Core/Core/Slippi/SlippiPad.cpp index b06bc19795..3538cb8f67 100644 --- a/Source/Core/Core/Slippi/SlippiPad.cpp +++ b/Source/Core/Core/Slippi/SlippiPad.cpp @@ -10,21 +10,24 @@ static u8 emptyPad[SLIPPI_PAD_FULL_SIZE] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; SlippiPad::SlippiPad(int32_t frame) { this->frame = frame; - memcpy(this->padBuf, emptyPad, SLIPPI_PAD_FULL_SIZE); + this->checksum = 0; + this->checksum_frame = 0; + memcpy(this->pad_buf, emptyPad, SLIPPI_PAD_FULL_SIZE); } -SlippiPad::SlippiPad(int32_t frame, u8* padBuf) : SlippiPad(frame) +SlippiPad::SlippiPad(int32_t frame, u8* pad_buf) : SlippiPad(frame) { // Overwrite the data portion of the pad - memcpy(this->padBuf, padBuf, SLIPPI_PAD_DATA_SIZE); + memcpy(this->pad_buf, pad_buf, SLIPPI_PAD_DATA_SIZE); } -SlippiPad::SlippiPad(int32_t frame, u8 playerIdx, u8* padBuf) : SlippiPad(frame) +SlippiPad::SlippiPad(int32_t frame, s32 checksum_frame, u32 checksum, u8* pad_buf) + : SlippiPad(frame, pad_buf) { - this->frame = frame; - this->playerIdx = playerIdx; + this->checksum_frame = checksum_frame; + this->checksum = checksum; // Overwrite the data portion of the pad - memcpy(this->padBuf, padBuf, SLIPPI_PAD_DATA_SIZE); + memcpy(this->pad_buf, pad_buf, SLIPPI_PAD_DATA_SIZE); } SlippiPad::~SlippiPad() diff --git a/Source/Core/Core/Slippi/SlippiPad.h b/Source/Core/Core/Slippi/SlippiPad.h index b8dfe31f35..deb3fe98d4 100644 --- a/Source/Core/Core/Slippi/SlippiPad.h +++ b/Source/Core/Core/Slippi/SlippiPad.h @@ -9,11 +9,12 @@ 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* pad_buf); + SlippiPad(int32_t frame, s32 checksum_frame, u32 checksum, u8* pad_buf); ~SlippiPad(); - int32_t frame; - u8 playerIdx; - u8 padBuf[SLIPPI_PAD_FULL_SIZE]; + s32 frame; + s32 checksum_frame; + u32 checksum; + u8 pad_buf[SLIPPI_PAD_FULL_SIZE]; }; diff --git a/Source/Core/DolphinQt/RenderWidget.cpp b/Source/Core/DolphinQt/RenderWidget.cpp index 3a9da28431..58be438da4 100644 --- a/Source/Core/DolphinQt/RenderWidget.cpp +++ b/Source/Core/DolphinQt/RenderWidget.cpp @@ -19,16 +19,16 @@ #include #include -//test ui stuff -#include +// test ui stuff #include +#include #include "imgui.h" #include "Core/Config/MainSettings.h" #include "Core/Core.h" -#include "Core/State.h" #include "Core/Slippi/SlippiPlayback.h" +#include "Core/State.h" #include "DolphinQt/Host.h" #include "DolphinQt/QtUtils/ModalMessageBox.h"