diff --git a/Source/Core/Common/Logging/Log.h b/Source/Core/Common/Logging/Log.h index f45157a5e2..adfd734e94 100644 --- a/Source/Core/Common/Logging/Log.h +++ b/Source/Core/Common/Logging/Log.h @@ -62,6 +62,7 @@ enum class LogType : int SLIPPI_ONLINE, SLIPPI_RUST_DEPENDENCIES, SLIPPI_RUST_EXI, + SLIPPI_RUST_GAME_REPORTER, SLIPPI_RUST_JUKEBOX, SP1, SYMBOLS, diff --git a/Source/Core/Common/Logging/LogManager.cpp b/Source/Core/Common/Logging/LogManager.cpp index 8e078e60c7..cce57d3bb9 100644 --- a/Source/Core/Common/Logging/LogManager.cpp +++ b/Source/Core/Common/Logging/LogManager.cpp @@ -166,6 +166,7 @@ LogManager::LogManager() m_log[LogType::SLIPPI_RUST_DEPENDENCIES] = {"SLIPPI_RUST_DEPENDENCIES", "[Rust] Slippi Dependencies", false, true}; m_log[LogType::SLIPPI_RUST_EXI] = {"SLIPPI_RUST_EXI", "[Rust] Slippi EXI", false, true}; + m_log[LogType::SLIPPI_RUST_GAME_REPORTER] = {"SLIPPI_RUST_GAME_REPORTER", "[Rust] Slippi Game Reporter", false, true}; m_log[LogType::SLIPPI_RUST_JUKEBOX] = {"SLIPPI_RUST_JUKEBOX", "[Rust] Slippi Jukebox", false, true}; m_log[LogType::SP1] = {"SP1", "Serial Port 1"}; diff --git a/Source/Core/Core/CMakeLists.txt b/Source/Core/Core/CMakeLists.txt index b5ccc687f3..7d6aa43620 100644 --- a/Source/Core/Core/CMakeLists.txt +++ b/Source/Core/Core/CMakeLists.txt @@ -534,8 +534,6 @@ add_library(core Slippi/SlippiUser.h Slippi/SlippiGame.cpp Slippi/SlippiGame.h - Slippi/SlippiGameReporter.cpp - Slippi/SlippiGameReporter.h Slippi/SlippiDirectCodes.cpp Slippi/SlippiPremadeText.h State.cpp diff --git a/Source/Core/Core/HW/EXI/EXI_Channel.h b/Source/Core/Core/HW/EXI/EXI_Channel.h index adf57eac06..64c3d96656 100644 --- a/Source/Core/Core/HW/EXI/EXI_Channel.h +++ b/Source/Core/Core/HW/EXI/EXI_Channel.h @@ -120,7 +120,7 @@ private: // it, as this class creates the CEXIMemoryCard instances. Memcard::HeaderData m_memcard_header_data; - // used by SlippiGameReporter for calculating the md5 + // used by game_reporter (rust) for calculating the md5 and Slippi Jukebox (rust) for playback std::string m_current_file_name; // Devices diff --git a/Source/Core/Core/HW/EXI/EXI_DeviceSlippi.cpp b/Source/Core/Core/HW/EXI/EXI_DeviceSlippi.cpp index 695c19fe0a..57f621fcd9 100644 --- a/Source/Core/Core/HW/EXI/EXI_DeviceSlippi.cpp +++ b/Source/Core/Core/HW/EXI/EXI_DeviceSlippi.cpp @@ -146,7 +146,6 @@ CEXISlippi::CEXISlippi(Core::System& system, const std::string current_file_name g_playbackStatus = std::make_unique(); matchmaking = std::make_unique(user.get()); gameFileLoader = std::make_unique(); - game_reporter = std::make_unique(user.get(), current_file_name); g_replayComm = std::make_unique(); directCodes = std::make_unique("direct-codes.json"); teamsCodes = std::make_unique("teams-codes.json"); @@ -265,9 +264,6 @@ CEXISlippi::~CEXISlippi() { u8 empty[1]; - // Instruct the Rust EXI device to shut down/drop everything. - slprs_exi_device_destroy(slprs_exi_device_ptr); - // Closes file gracefully to prevent file corruption when emulation // suddenly stops. This would happen often on netplay when the opponent // would close the emulation before the file successfully finished writing @@ -287,7 +283,9 @@ CEXISlippi::~CEXISlippi() if (active_match_id.find("mode.ranked") != std::string::npos) { ERROR_LOG_FMT(SLIPPI_ONLINE, "Exit during in-progress ranked game: {}", active_match_id); - game_reporter->ReportAbandonment(active_match_id); + auto user_info = user->GetUserInfo(); + slprs_exi_device_report_match_abandonment(slprs_exi_device_ptr, user_info.uid.c_str(), + user_info.play_key.c_str(), active_match_id.c_str()); } handleConnectionCleanup(); @@ -295,6 +293,9 @@ CEXISlippi::~CEXISlippi() g_playbackStatus->resetPlayback(); + // Instruct the Rust EXI device to shut down/drop everything. + slprs_exi_device_destroy(slprs_exi_device_ptr); + // TODO: ENET shutdown should maybe be done at app shutdown instead. // Right now this might be problematic in the case where someone starts a netplay client // and then queues into online matchmaking, and then stops the game. That might deinit @@ -358,7 +359,7 @@ std::vector CEXISlippi::generateMetadata() // Add game start time u8 dateTimeStrLength = sizeof "2011-10-08T07:07:09Z"; std::vector dateTimeBuf(dateTimeStrLength); - strftime(&dateTimeBuf[0], dateTimeStrLength, "%FT%TZ", gmtime(&gameStartTime)); + strftime(&dateTimeBuf[0], dateTimeStrLength, "%FT%TZ", gmtime(&game_start_time)); dateTimeBuf.pop_back(); // Removes the \0 from the back of string metadata.insert(metadata.end(), {'U', 7, 's', 't', 'a', 'r', 't', 'A', 't', 'S', 'U', static_cast(dateTimeBuf.size())}); @@ -519,12 +520,12 @@ void CEXISlippi::writeToFile(std::unique_ptr msg) // Get display names and connection codes from slippi netplay client if (slippi_netplay) { - auto playerInfo = matchmaking->GetPlayerInfo(); + auto player_info = matchmaking->GetPlayerInfo(); - for (int i = 0; i < playerInfo.size(); i++) + for (int i = 0; i < player_info.size(); i++) { - slippi_names[i] = playerInfo[i].display_name; - slippi_connect_codes[i] = playerInfo[i].connect_code; + slippi_names[i] = player_info[i].display_name; + slippi_connect_codes[i] = player_info[i].connect_code; } } } @@ -610,7 +611,7 @@ void CEXISlippi::createNewFile() // Append YYYY-MM to the directory path uint8_t yearMonthStrLength = sizeof "2020-06-Mainline"; std::vector yearMonthBuf(yearMonthStrLength); - strftime(&yearMonthBuf[0], yearMonthStrLength, "%Y-%m-Mainline", localtime(&gameStartTime)); + strftime(&yearMonthBuf[0], yearMonthStrLength, "%Y-%m-Mainline", localtime(&game_start_time)); std::string yearMonth(&yearMonthBuf[0]); dirpath.append(yearMonth); @@ -644,7 +645,7 @@ std::string CEXISlippi::generateFileName() // Add game start time u8 dateTimeStrLength = sizeof "20171015T095717"; std::vector dateTimeBuf(dateTimeStrLength); - strftime(&dateTimeBuf[0], dateTimeStrLength, "%Y%m%dT%H%M%S", localtime(&gameStartTime)); + strftime(&dateTimeBuf[0], dateTimeStrLength, "%Y%m%dT%H%M%S", localtime(&game_start_time)); std::string str(&dateTimeBuf[0]); return StringFromFormat("Game_%s.slp", str.c_str()); @@ -1464,7 +1465,7 @@ bool CEXISlippi::shouldAdvanceOnlineFrame(s32 frame) bool isSlow = (offsetUs < -t1 && fallBehindCounter > 50) || (offsetUs < -t2 && fallFarBehindCounter > 15); - if (isSlow && lastSearch.mode != SlippiMatchmaking::OnlinePlayMode::TEAMS) + if (isSlow && last_search.mode != SlippiMatchmaking::OnlinePlayMode::TEAMS) { // We don't show this message for teams because it seems to false positive a lot there, maybe // because the min offset is always selected? Idk I feel like doubles has some perf issues I @@ -1569,7 +1570,7 @@ void CEXISlippi::prepareOpponentInputs(s32 frame, bool shouldSkip) 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]->checksum_frame, + // INFO_LOG_FMT(SLIPPI_ONLINE, "Sending checksum values: [{}] %08x", results[i]->checksum_frame, // results[i]->checksum); appendWordToBuffer(&m_read_queue, static_cast(results[i]->checksum_frame)); appendWordToBuffer(&m_read_queue, results[i]->checksum); @@ -1672,7 +1673,7 @@ void CEXISlippi::handleCaptureSavestate(u8* payload) activeSavestates[frame] = std::move(ss); // u32 timeDiff = (u32)(Common::Timer::NowUs() - startTime); - // INFO_LOG_FMT(SLIPPI_ONLINE, "SLIPPI ONLINE: Captured savestate for frame {} in: %f ms", frame, + // INFO_LOG_FMT(SLIPPI_ONLINE, "SLIPPI ONLINE: Captured savestate for frame {} in: {} ms", frame, // ((double)timeDiff) / 1000); } @@ -1745,7 +1746,7 @@ void CEXISlippi::startFindMatch(u8* payload) search.connectCode = shiftJisCode; // Store this search so we know what was queued for - lastSearch = search; + last_search = search; // While we do have another condition that checks characters after being connected, it's nice to // give someone an early error before they even queue so that they wont enter the queue and make @@ -1912,13 +1913,13 @@ void CEXISlippi::handleNameEntryLoad(u8* payload) appendWordToBuffer(&m_read_queue, curIndex); } -// teamId 0 = red, 1 = blue, 2 = green -int CEXISlippi::getCharColor(u8 charId, u8 teamId) +// team_id 0 = red, 1 = blue, 2 = green +int CEXISlippi::getCharColor(u8 char_id, u8 team_id) { - switch (charId) + switch (char_id) { case 0x0: // Falcon - switch (teamId) + switch (team_id) { case 0: return 2; @@ -1928,7 +1929,7 @@ int CEXISlippi::getCharColor(u8 charId, u8 teamId) return 4; } case 0x2: // Fox - switch (teamId) + switch (team_id) { case 0: return 1; @@ -1938,7 +1939,7 @@ int CEXISlippi::getCharColor(u8 charId, u8 teamId) return 3; } case 0xC: // Peach - switch (teamId) + switch (team_id) { case 0: return 0; @@ -1948,7 +1949,7 @@ int CEXISlippi::getCharColor(u8 charId, u8 teamId) return 4; } case 0x13: // Sheik - switch (teamId) + switch (team_id) { case 0: return 1; @@ -1958,7 +1959,7 @@ int CEXISlippi::getCharColor(u8 charId, u8 teamId) return 3; } case 0x14: // Falco - switch (teamId) + switch (team_id) { case 0: return 1; @@ -2037,8 +2038,8 @@ void CEXISlippi::prepareOnlineMatchState() // 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. - recentMmResult = matchmaking->GetMatchmakeResult(); - allowedStages = recentMmResult.stages; + recent_mm_result = matchmaking->GetMatchmakeResult(); + allowedStages = recent_mm_result.stages; // Clear stage pool so that when we call getRandomStage it will use full list stagePool.clear(); localSelections.stageId = getRandomStage(); @@ -2093,7 +2094,7 @@ void CEXISlippi::prepareOnlineMatchState() // Here we are connected, check to see if we should init play session if (!is_play_session_active) { - game_reporter->StartNewSession(); + slprs_exi_device_start_new_reporter_session(slprs_exi_device_ptr); is_play_session_active = true; } } @@ -2194,7 +2195,7 @@ void CEXISlippi::prepareOnlineMatchState() rps[i].isCharacterSelected = true; } - remotePlayerCount = lastSearch.mode == SlippiMatchmaking::OnlinePlayMode::TEAMS ? 3 : 1; + remotePlayerCount = last_search.mode == SlippiMatchmaking::OnlinePlayMode::TEAMS ? 3 : 1; oppName = std::string("Player"); #endif @@ -2245,7 +2246,7 @@ void CEXISlippi::prepareOnlineMatchState() break; } - if (SlippiMatchmaking::IsFixedRulesMode(lastSearch.mode)) + if (SlippiMatchmaking::IsFixedRulesMode(last_search.mode)) { // If we enter one of these conditions, someone is doing something bad, clear the lobby if (!localCharOk) @@ -2270,7 +2271,7 @@ void CEXISlippi::prepareOnlineMatchState() return; } } - else if (lastSearch.mode == SlippiMatchmaking::OnlinePlayMode::TEAMS) + else if (last_search.mode == SlippiMatchmaking::OnlinePlayMode::TEAMS) { auto isMEX = SConfig::GetSlippiConfig().melee_version == Melee::Version::MEX; @@ -2363,10 +2364,10 @@ void CEXISlippi::prepareOnlineMatchState() *stage = Common::swap16(stageId); // Turn pause off in unranked/ranked, on in other modes - auto pauseAllowed = lastSearch.mode == SlippiMatchmaking::OnlinePlayMode::DIRECT; - u8* gameBitField3 = static_cast(&onlineMatchBlock[2]); - *gameBitField3 = pauseAllowed ? *gameBitField3 & 0xF7 : *gameBitField3 | 0x8; - //*gameBitField3 = *gameBitField3 | 0x8; + auto pause_allowed = last_search.mode == SlippiMatchmaking::OnlinePlayMode::DIRECT; + u8* game_bit_field3 = static_cast(&onlineMatchBlock[2]); + *game_bit_field3 = pause_allowed ? *game_bit_field3 & 0xF7 : *game_bit_field3 | 0x8; + //*game_bit_field3 = *game_bit_field3 | 0x8; // Group players into left/right side for team splash screen display for (int i = 0; i < 4; i++) @@ -2457,10 +2458,10 @@ void CEXISlippi::prepareOnlineMatchState() std::string defaultConnectCodes[] = {"PLYR#001", "PLYR#002", "PLYR#003", "PLYR#004"}; #endif - auto playerInfo = matchmaking->GetPlayerInfo(); + auto player_info = matchmaking->GetPlayerInfo(); for (int i = 0; i < 4; i++) { - std::string connectCode = i < playerInfo.size() ? playerInfo[i].connect_code : ""; + std::string connectCode = i < player_info.size() ? player_info[i].connect_code : ""; #ifdef LOCAL_TESTING connectCode = defaultConnectCodes[i]; #endif @@ -2475,7 +2476,7 @@ void CEXISlippi::prepareOnlineMatchState() for (int i = 0; i < 4; i++) { - std::string uid = i < playerInfo.size() ? playerInfo[i].uid : + std::string uid = i < player_info.size() ? player_info[i].uid : ""; // UIDs are 28 characters + 1 null terminator #ifdef LOCAL_TESTING uid = defaultUids[i]; @@ -2493,9 +2494,9 @@ void CEXISlippi::prepareOnlineMatchState() m_read_queue.insert(m_read_queue.end(), onlineMatchBlock.begin(), onlineMatchBlock.end()); // Add match id to output - std::string matchId = recentMmResult.id; - matchId.resize(51); - m_read_queue.insert(m_read_queue.end(), matchId.begin(), matchId.end()); + std::string match_id = recent_mm_result.id; + match_id.resize(51); + m_read_queue.insert(m_read_queue.end(), match_id.begin(), match_id.end()); } u16 CEXISlippi::getRandomStage() @@ -2527,7 +2528,7 @@ void CEXISlippi::setMatchSelections(u8* payload) s.stageId = Common::swap16(&payload[4]); u8 stageSelectOption = payload[6]; - // u8 onlineMode = payload[7]; + // u8 online_mode = payload[7]; s.isStageSelected = stageSelectOption == 1 || stageSelectOption == 3; if (stageSelectOption == 3) @@ -2675,7 +2676,7 @@ bool CEXISlippi::isSlippiChatEnabled() { auto chatEnabledChoice = Config::Get(Config::SLIPPI_ENABLE_QUICK_CHAT); bool res = true; - switch (lastSearch.mode) + switch (last_search.mode) { case SlippiMatchmaking::DIRECT: res = chatEnabledChoice == Slippi::Chat::ON || chatEnabledChoice == Slippi::Chat::DIRECT_ONLY; @@ -2829,49 +2830,75 @@ void CEXISlippi::prepareNewSeed() void CEXISlippi::handleReportGame(const SlippiExiTypes::ReportGameQuery& query) { - SlippiGameReporter::GameReport r; - 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; + std::string match_id = recent_mm_result.id; + SlippiMatchmakingOnlinePlayMode online_mode = static_cast(query.mode); + u32 duration_frames = query.frame_length; + u32 game_index = query.game_index; + u32 tiebreak_index = query.tiebreak_index; + s8 winner_idx = query.winner_idx; + int stage_id = Common::FromBigEndian(*(u16 *)&query.game_info_block[0xE]); + u8 game_end_method = query.game_end_method; + s8 lras_initiator = query.lras_initiator; - ERROR_LOG_FMT(SLIPPI_ONLINE, - "Mode: {} / {}, Frames: {}, GameIdx: {}, TiebreakIdx: {}, WinnerIdx: {}, " - "StageId: {}, GameEndMethod: {}, LRASInitiator: {}", - static_cast(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); + ERROR_LOG_FMT(SLIPPI_ONLINE, + "Mode: {} / {}, Frames: {}, GameIdx: {}, TiebreakIdx: {}, WinnerIdx: {}, StageId: {}, GameEndMethod: {}, " + "LRASInitiator: {}", + static_cast(online_mode), query.mode, duration_frames, game_index, tiebreak_index, winner_idx, stage_id, game_end_method, + lras_initiator); - auto mm_players = recentMmResult.players; + auto user_info = user->GetUserInfo(); - for (auto i = 0; i < 4; ++i) - { - SlippiGameReporter::PlayerReport p; - 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]); + // We pass `uid` and `playKey` here until the User side of things is + // ported to Rust. + uintptr_t game_report = slprs_game_report_create(user_info.uid.c_str(), user_info.play_key.c_str(), online_mode, + match_id.c_str(), duration_frames, game_index, tiebreak_index, + winner_idx, game_end_method, lras_initiator, stage_id); - 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); + auto mm_players = recent_mm_result.players; - r.players.push_back(p); - } + for (auto i = 0; i < 4; ++i) + { + std::string uid = mm_players.size() > i ? mm_players[i].uid : ""; + u8 slot_type = query.players[i].slot_type; + u8 stocks_remaining = query.players[i].stocks_remaining; + float damage_done = query.players[i].damage_done; + u8 char_id = query.game_info_block[0x60 + 0x24 * i]; + u8 color_id = query.game_info_block[0x63 + 0x24 * i]; + int starting_stocks = query.game_info_block[0x62 + 0x24 * i]; + int starting_percent = Common::FromBigEndian(*(u16 *)&query.game_info_block[0x70 + 0x24 * i]); + + ERROR_LOG_FMT(SLIPPI_ONLINE, + "UID: {}, Port Type: {}, Stocks: {}, DamageDone: {}, CharId: {}, ColorId: {}, StartStocks: {}, " + "StartPercent: {}", + uid.c_str(), slot_type, stocks_remaining, damage_done, char_id, color_id, starting_stocks, starting_percent); + + uintptr_t player_report = slprs_player_report_create(uid.c_str(), slot_type, damage_done, stocks_remaining, char_id, + color_id, starting_stocks, starting_percent); + + slprs_game_report_add_player_report(game_report, player_report); + } + + // If ranked mode and the game ended with a quit out, this is either a desync or an interrupted game, + // attempt to send synced values to opponents in order to restart the match where it was left off + if (online_mode == SlippiMatchmakingOnlinePlayMode::Ranked && game_end_method == 7) + { + SlippiSyncedGameState s; + s.match_id = match_id; + s.game_index = game_index; + s.tiebreak_index = tiebreak_index; + s.seconds_remaining = query.synced_timer; + for (int i = 0; i < 4; i++) + { + s.fighters[i].stocks_remaining = query.players[i].synced_stocks_remaining; + s.fighters[i].current_health = query.players[i].synced_current_health; + } + + if (slippi_netplay) + slippi_netplay->SendSyncedGameState(s); + } #ifndef LOCAL_TESTING - game_reporter->StartReport(r); + slprs_exi_device_log_game_report(slprs_exi_device_ptr, game_report); #endif } @@ -2971,11 +2998,15 @@ void CEXISlippi::handleCompleteSet(const SlippiExiTypes::ReportSetCompletionQuer { ERROR_LOG_FMT(SLIPPI_ONLINE, "Hello"); - auto lastMatchId = recentMmResult.id; - if (lastMatchId.find("mode.ranked") != std::string::npos) + auto last_match_id = recent_mm_result.id; + if (last_match_id.find("mode.ranked") != std::string::npos) { - INFO_LOG_FMT(SLIPPI_ONLINE, "Reporting set completion: {}", lastMatchId); - game_reporter->ReportCompletion(lastMatchId, query.endMode); + INFO_LOG_FMT(SLIPPI_ONLINE, "Reporting set completion: {}", last_match_id); + auto user_info = user->GetUserInfo(); + + slprs_exi_device_report_match_completion(slprs_exi_device_ptr, user_info.uid.c_str(), + user_info.play_key.c_str(), last_match_id.c_str(), + query.end_mode); } } @@ -2985,29 +3016,29 @@ void CEXISlippi::handleGetPlayerSettings() SlippiExiTypes::GetPlayerSettingsResponse resp = {}; - std::vector> messagesByPlayer = { + std::vector> messages_by_player = { SlippiUser::default_chat_messages, SlippiUser::default_chat_messages, SlippiUser::default_chat_messages, SlippiUser::default_chat_messages}; // These chat messages will be used when previewing messages - auto userChatMessages = user->GetUserInfo().chat_messages; - if (userChatMessages.size() == 16) + auto user_chat_messages = user->GetUserInfo().chat_messages; + if (user_chat_messages.size() == 16) { - messagesByPlayer[0] = userChatMessages; + messages_by_player[0] = user_chat_messages; } // These chat messages will be set when we have an opponent. We load their and our messages - auto playerInfo = matchmaking->GetPlayerInfo(); - for (auto& player : playerInfo) + auto player_info = matchmaking->GetPlayerInfo(); + for (auto& player : player_info) { - messagesByPlayer[player.port - 1] = player.chat_messages; + messages_by_player[player.port - 1] = player.chat_messages; } for (int i = 0; i < 4; i++) { for (int j = 0; j < 16; j++) { - auto str = ConvertStringForGame(messagesByPlayer[i][j], MAX_MESSAGE_LENGTH); + auto str = ConvertStringForGame(messages_by_player[i][j], MAX_MESSAGE_LENGTH); sprintf(resp.settings[i].chatMessages[j], "%s", str.c_str()); } } @@ -3039,15 +3070,16 @@ void CEXISlippi::DMAWrite(u32 _uAddr, u32 _uSize) u8 byte = memPtr[0]; if (byte == CMD_RECEIVE_COMMANDS) { - time(&gameStartTime); // Store game start time - u8 receiveCommandsLen = memPtr[1]; - configureCommands(&memPtr[1], receiveCommandsLen); - writeToFileAsync(&memPtr[0], receiveCommandsLen + 1, "create"); - bufLoc += receiveCommandsLen + 1; + time(&game_start_time); // Store game start time + u8 receive_commands_len = memPtr[1]; + configureCommands(&memPtr[1], receive_commands_len); + writeToFileAsync(&memPtr[0], receive_commands_len + 1, "create"); + bufLoc += receive_commands_len + 1; g_needInputForFrame = true; SlippiSpectateServer::getInstance().startGame(); - SlippiSpectateServer::getInstance().write(&memPtr[0], receiveCommandsLen + 1); - game_reporter->PushReplayData(&memPtr[0], receiveCommandsLen + 1, "create"); + SlippiSpectateServer::getInstance().write(&memPtr[0], receive_commands_len + 1); + slprs_exi_device_reporter_push_replay_data(slprs_exi_device_ptr, &memPtr[0], + receive_commands_len + 1); } if (byte == CMD_MENU_FRAME) @@ -3081,7 +3113,8 @@ void CEXISlippi::DMAWrite(u32 _uAddr, u32 _uSize) writeToFileAsync(&memPtr[bufLoc], payloadLen + 1, "close"); SlippiSpectateServer::getInstance().write(&memPtr[bufLoc], payloadLen + 1); SlippiSpectateServer::getInstance().endGame(); - game_reporter->PushReplayData(&memPtr[bufLoc], payloadLen + 1, "close"); + slprs_exi_device_reporter_push_replay_data(slprs_exi_device_ptr, &memPtr[bufLoc], + payloadLen + 1); break; case CMD_PREPARE_REPLAY: // log.open("log.txt"); @@ -3094,7 +3127,8 @@ void CEXISlippi::DMAWrite(u32 _uAddr, u32 _uSize) g_needInputForFrame = true; writeToFileAsync(&memPtr[bufLoc], payloadLen + 1, ""); SlippiSpectateServer::getInstance().write(&memPtr[bufLoc], payloadLen + 1); - game_reporter->PushReplayData(&memPtr[bufLoc], payloadLen + 1, ""); + slprs_exi_device_reporter_push_replay_data(slprs_exi_device_ptr, &memPtr[bufLoc], + payloadLen + 1); break; case CMD_IS_STOCK_STEAL: prepareIsStockSteal(&memPtr[bufLoc + 1]); @@ -3198,7 +3232,8 @@ void CEXISlippi::DMAWrite(u32 _uAddr, u32 _uSize) default: writeToFileAsync(&memPtr[bufLoc], payloadLen + 1, ""); SlippiSpectateServer::getInstance().write(&memPtr[bufLoc], payloadLen + 1); - game_reporter->PushReplayData(&memPtr[bufLoc], payloadLen + 1, ""); + slprs_exi_device_reporter_push_replay_data(slprs_exi_device_ptr, &memPtr[bufLoc], + payloadLen + 1); break; } @@ -3248,8 +3283,8 @@ void CEXISlippi::ConfigureJukebox() auto& system = Core::System::GetInstance(); slprs_exi_device_configure_jukebox(slprs_exi_device_ptr, - Config::Get(Config::SLIPPI_ENABLE_JUKEBOX), system.GetMemory().GetRAM(), - AudioCommonGetCurrentVolume); + Config::Get(Config::SLIPPI_ENABLE_JUKEBOX), + system.GetMemory().GetRAM(), AudioCommonGetCurrentVolume); #endif } diff --git a/Source/Core/Core/HW/EXI/EXI_DeviceSlippi.h b/Source/Core/Core/HW/EXI/EXI_DeviceSlippi.h index 8fc97ef87c..2083eacd39 100644 --- a/Source/Core/Core/HW/EXI/EXI_DeviceSlippi.h +++ b/Source/Core/Core/HW/EXI/EXI_DeviceSlippi.h @@ -11,7 +11,6 @@ #include "Core/Slippi/SlippiExiTypes.h" #include "Core/Slippi/SlippiGame.h" #include "Core/Slippi/SlippiGameFileLoader.h" -#include "Core/Slippi/SlippiGameReporter.h" #include "Core/Slippi/SlippiMatchmaking.h" #include "Core/Slippi/SlippiNetplay.h" #include "Core/Slippi/SlippiPlayback.h" @@ -168,7 +167,7 @@ private: u32 writtenByteCount = 0; // vars for metadata generation - time_t gameStartTime; + time_t game_start_time; s32 lastFrame; std::unordered_map> characterUsage; @@ -255,8 +254,8 @@ private: std::vector m_read_queue; std::unique_ptr m_current_game = nullptr; SlippiSpectateServer* m_slippiserver = nullptr; - SlippiMatchmaking::MatchSearchSettings lastSearch; - SlippiMatchmaking::MatchmakeResult recentMmResult; + SlippiMatchmaking::MatchSearchSettings last_search; + SlippiMatchmaking::MatchmakeResult recent_mm_result; std::vector stagePool; @@ -299,7 +298,6 @@ private: std::unique_ptr gameFileLoader; std::unique_ptr slippi_netplay; std::unique_ptr matchmaking; - std::unique_ptr game_reporter; std::unique_ptr directCodes; std::unique_ptr teamsCodes; diff --git a/Source/Core/Core/Slippi/SlippiExiTypes.h b/Source/Core/Core/Slippi/SlippiExiTypes.h index ecb46ae4f7..bcae0e6f13 100644 --- a/Source/Core/Core/Slippi/SlippiExiTypes.h +++ b/Source/Core/Core/Slippi/SlippiExiTypes.h @@ -40,7 +40,7 @@ struct ReportGameQuery struct ReportSetCompletionQuery { u8 command; - u8 endMode; + u8 end_mode; }; struct GpCompleteStepQuery diff --git a/Source/Core/Core/Slippi/SlippiGameReporter.cpp b/Source/Core/Core/Slippi/SlippiGameReporter.cpp deleted file mode 100644 index 0de88b9368..0000000000 --- a/Source/Core/Core/Slippi/SlippiGameReporter.cpp +++ /dev/null @@ -1,458 +0,0 @@ -#include "SlippiGameReporter.h" - -#include "Common/Common.h" -#include "Common/CommonPaths.h" -#include "Common/FileUtil.h" -#include "Common/Logging/Log.h" -#include "Common/MsgHandler.h" -#include "Common/StringUtil.h" -#include "Common/Thread.h" -#include "Core/ConfigManager.h" -#include "Core/Slippi/SlippiMatchmaking.h" -#include "VideoCommon/OnScreenDisplay.h" - -#include -#include - -#include -#include -#include -#include -using json = nlohmann::json; - -static std::array s_MD5; -static const mbedtls_md_info_t* s_md5_info = mbedtls_md_info_from_type(MBEDTLS_MD_MD5); - -static size_t curl_receive(char* ptr, size_t size, size_t nmemb, void* rcvBuf) -{ - size_t len = size * nmemb; - INFO_LOG_FMT(SLIPPI_ONLINE, "[GameReport] Received data: {}", len); - - std::string* buf = (std::string*)rcvBuf; - - buf->insert(buf->end(), ptr, ptr + len); - return len; -} - -static size_t curl_send(char* ptr, size_t size, size_t nmemb, void* userdata) -{ - std::vector* buf = (std::vector*)userdata; - - INFO_LOG_FMT(SLIPPI_ONLINE, "[GameReport] Sending data. Size: {}, Nmemb: {}. Buffer length: {}", - size, nmemb, buf->size()); - - size_t copy_size = size * nmemb; - if (copy_size > buf->size()) - copy_size = buf->size(); - - if (copy_size == 0) - return 0; - - // This method of reading from a vector seems so jank, im sure there's better ways to do this - memcpy(ptr, &buf->at(0), copy_size); - buf->erase(buf->begin(), buf->begin() + copy_size); - - return copy_size; -} - -SlippiGameReporter::SlippiGameReporter(SlippiUser* user, const std::string current_file_name) -{ - CURL* curl = curl_easy_init(); - if (curl) - { - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &curl_receive); - curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 10000); - // Set up HTTP Headers - m_curl_header_list = curl_slist_append(m_curl_header_list, "Content-Type: application/json"); - curl_easy_setopt(curl, CURLOPT_HTTPHEADER, m_curl_header_list); - - curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, m_curl_err_buf); - curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L); -#ifdef _WIN32 - // ALPN support is enabled by default but requires Windows >= 8.1. - curl_easy_setopt(curl, CURLOPT_SSL_ENABLE_ALPN, false); -#endif - m_curl = curl; - } - - CURL* curl_upload = curl_easy_init(); - if (curl_upload) - { - curl_easy_setopt(curl_upload, CURLOPT_READFUNCTION, &curl_send); - curl_easy_setopt(curl_upload, CURLOPT_UPLOAD, 1L); - curl_easy_setopt(curl_upload, CURLOPT_WRITEFUNCTION, &curl_receive); - curl_easy_setopt(curl_upload, CURLOPT_TIMEOUT_MS, 10000); - curl_easy_setopt(curl_upload, CURLOPT_ERRORBUFFER, m_curl_upload_err_buf); - curl_easy_setopt(curl_upload, CURLOPT_FAILONERROR, 1L); - - // Set up HTTP Headers - m_curl_upload_headers = - curl_slist_append(m_curl_upload_headers, "Content-Type: application/octet-stream"); - curl_slist_append(m_curl_upload_headers, "Content-Encoding: gzip"); - curl_slist_append(m_curl_upload_headers, "X-Goog-Content-Length-Range: 0,10000000"); - curl_easy_setopt(curl_upload, CURLOPT_HTTPHEADER, m_curl_upload_headers); - -#ifdef _WIN32 - // ALPN support is enabled by default but requires Windows >= 8.1. - curl_easy_setopt(curl_upload, CURLOPT_SSL_ENABLE_ALPN, false); -#endif - - m_curl_upload = curl_upload; - } - - m_user = user; - - run_thread = true; - reporting_thread = std::thread(&SlippiGameReporter::ReportThreadHandler, this); - - m_md5_thread = std::thread([this, current_file_name]() { - if (!run_thread) { - return; - } - - mbedtls_md_file(s_md5_info, current_file_name.c_str(), s_MD5.data()); - - std::string output{}; - for (u8 n : s_MD5) - { - output += StringFromFormat("%02x", n); - } - this->m_iso_hash = output; - - if (!run_thread) { - return; - } - - if (known_desync_isos.find(this->m_iso_hash) != known_desync_isos.end() && - known_desync_isos.at(this->m_iso_hash)) - { - OSD::AddTypedMessage(OSD::MessageType::DesyncWarning, - "\n\n\n\nCAUTION: You are using an ISO that is known to cause desyncs", - 20000, OSD::Color::RED); - } - INFO_LOG_FMT(SLIPPI_ONLINE, "MD5 Hash: {}", this->m_iso_hash); - }); - m_md5_thread.detach(); -} - -SlippiGameReporter::~SlippiGameReporter() -{ - run_thread = false; - cv.notify_one(); - if (reporting_thread.joinable()) - reporting_thread.join(); - - if (m_md5_thread.joinable()) - m_md5_thread.join(); - - if (m_curl) - { - curl_slist_free_all(m_curl_header_list); - curl_easy_cleanup(m_curl); - } - - if (m_curl_upload) - { - curl_slist_free_all(m_curl_upload_headers); - curl_easy_cleanup(m_curl_upload); - } -} - -void SlippiGameReporter::PushReplayData(u8* data, u32 length, std::string action) -{ - if (action == "create") - { - m_replay_write_idx += 1; - } - - // This makes a vector at this index if it doesn't exist - auto& v = m_replay_data[m_replay_write_idx]; - - // Insert new data into vector - v.insert(v.end(), data, data + length); - - if (action == "close") - { - m_replay_last_completed_idx = m_replay_write_idx; - } -} - -void SlippiGameReporter::StartReport(GameReport report) -{ - game_report_queue.emplace(report); - cv.notify_one(); -} - -void SlippiGameReporter::StartNewSession() -{ - // Maybe we could do stuff here? We used to initialize gameIndex but that isn't required anymore -} - -void SlippiGameReporter::ReportThreadHandler() -{ - std::unique_lock lck(mtx); - - while (run_thread) - { - // Wait for report to come in - cv.wait(lck); - - auto queue_has_data = !game_report_queue.empty(); - - // Process all messages - while (!game_report_queue.empty()) - { - auto& report = game_report_queue.front(); - report.report_attempts += 1; - - auto is_first_attempt = report.report_attempts == 1; - auto is_last_attempt = report.report_attempts >= 5; // Only do five attempts - auto error_sleep_ms = is_last_attempt ? 0 : report.report_attempts * 100; - - // If the thread is shutting down, give up after one attempt - if (!run_thread && !is_first_attempt) - { - game_report_queue.pop(); - continue; - } - - // auto ranked = SlippiMatchmaking::OnlinePlayMode::RANKED; - - auto user_info = m_user->GetUserInfo(); - WARN_LOG_FMT(SLIPPI_ONLINE, "Checking game report for game {}. Length: {}...", - report.game_index, report.duration_frames); - - // Prepare report - json request; - request["matchId"] = report.match_id; - request["uid"] = user_info.uid; - request["playKey"] = user_info.play_key; - request["mode"] = report.mode; - request["gameIndex"] = report.game_index; - request["tiebreakIndex"] = report.tiebreak_index; - request["gameIndex"] = report.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; - request["isoHash"] = m_iso_hash; - - json players = json::array(); - for (int i = 0; i < report.players.size(); i++) - { - json p; - p["uid"] = report.players[i].uid; - 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; - } - - request["players"] = players; - // Just pop before request if this is the last attempt - if (is_last_attempt) - { - game_report_queue.pop(); - } - - auto request_string = request.dump(); - - // Send report - std::string resp; - curl_easy_setopt(m_curl, CURLOPT_POST, true); - curl_easy_setopt(m_curl, CURLOPT_URL, REPORT_URL.c_str()); - curl_easy_setopt(m_curl, CURLOPT_POSTFIELDS, request_string.c_str()); - curl_easy_setopt(m_curl, CURLOPT_POSTFIELDSIZE, request_string.length()); - curl_easy_setopt(m_curl, CURLOPT_WRITEDATA, &resp); - CURLcode res = curl_easy_perform(m_curl); - - if (res != 0) - { - ERROR_LOG_FMT(SLIPPI_ONLINE, "[GameReport] Got error executing request. Err code : {}", - static_cast(res)); - Common::SleepCurrentThread(error_sleep_ms); - continue; - } - - long response_code; - curl_easy_getinfo(m_curl, CURLINFO_RESPONSE_CODE, &response_code); - if (response_code != 200) - { - ERROR_LOG_FMT(SLIPPI_ONLINE, "[GameReport] Server responded with non-success status: {}", - response_code); - Common::SleepCurrentThread(error_sleep_ms); - continue; - } - - // Check if response is valid json - if (!json::accept(resp)) - { - ERROR_LOG_FMT(SLIPPI, "[GameReport] Server responded with invalid json: {}", resp); - Common::SleepCurrentThread(error_sleep_ms); - continue; - } - - // Parse the response - auto r = json::parse(resp); - if (!r.is_object()) - { - ERROR_LOG_FMT(SLIPPI, "JSON was not an object. {}", resp); - Common::SleepCurrentThread(error_sleep_ms); - continue; - } - bool success = r.value("success", false); - if (!success) - { - ERROR_LOG_FMT(SLIPPI_ONLINE, "[GameReport] Report reached server but failed. {}", - resp.c_str()); - Common::SleepCurrentThread(error_sleep_ms); - continue; - } - - // If this was not the last attempt, pop if we are successful. On the last attempt pop will - // already have happened - if (!is_last_attempt) - { - game_report_queue.pop(); - } - - std::string upload_url = r.value("uploadUrl", ""); - UploadReplay(m_replay_last_completed_idx, upload_url); - - Common::SleepCurrentThread(0); - } - - // Clean up replay data for games that are complete - if (queue_has_data) - { - auto firstIdx = m_replay_data.begin()->first; - for (int i = firstIdx; i < m_replay_last_completed_idx; i++) - { - INFO_LOG_FMT(SLIPPI_ONLINE, "Cleaning index {} in replay data.", i); - m_replay_data[i].clear(); - m_replay_data.erase(i); - } - } - } -} - -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 request_string = 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, request_string.c_str()); - curl_easy_setopt(m_curl, CURLOPT_POSTFIELDSIZE, request_string.length()); - CURLcode res = curl_easy_perform(m_curl); - - if (res != 0) - { - ERROR_LOG_FMT(SLIPPI_ONLINE, - "[GameReport] Got error executing abandonment request. Err code: {}", - static_cast(res)); - } -} - -void SlippiGameReporter::ReportCompletion(std::string matchId, u8 endMode) -{ - auto userInfo = m_user->GetUserInfo(); - - // Prepare report - json request; - request["matchId"] = matchId; - request["uid"] = userInfo.uid; - request["playKey"] = userInfo.play_key; - request["endMode"] = endMode; - - auto request_string = request.dump(); - - // Send report - curl_easy_setopt(m_curl, CURLOPT_POST, true); - curl_easy_setopt(m_curl, CURLOPT_URL, COMPLETE_URL.c_str()); - curl_easy_setopt(m_curl, CURLOPT_POSTFIELDS, request_string.c_str()); - curl_easy_setopt(m_curl, CURLOPT_POSTFIELDSIZE, request_string.length()); - CURLcode res = curl_easy_perform(m_curl); - - if (res != 0) - { - ERROR_LOG_FMT(SLIPPI_ONLINE, - "[GameReport] Got error executing completion request. Err code: {}. Msg: {}", - static_cast(res), m_curl_err_buf); - } -} - -// https://stackoverflow.com/a/57699371/1249024 -int compressToGzip(const char* input, size_t inputSize, char* output, size_t outputSize) -{ - z_stream zs; - zs.zalloc = Z_NULL; - zs.zfree = Z_NULL; - zs.opaque = Z_NULL; - zs.avail_in = (uInt)inputSize; - zs.next_in = (Bytef*)input; - zs.avail_out = (uInt)outputSize; - zs.next_out = (Bytef*)output; - - // hard to believe they don't have a macro for gzip encoding, "Add 16" is the best thing zlib can - // do: "Add 16 to windowBits to write a simple gzip header and trailer around the compressed data - // instead of a zlib wrapper" - deflateInit2(&zs, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 15 | 16, 8, Z_DEFAULT_STRATEGY); - deflate(&zs, Z_FINISH); - deflateEnd(&zs); - return zs.total_out; -} - -void SlippiGameReporter::UploadReplay(int idx, std::string url) -{ - if (url.length() <= 0) - return; - - // INFO_LOG(SLIPPI_ONLINE, "Uploading replay: {}, {}", idx, url.c_str()); - - auto replay_data = m_replay_data[idx]; - u32 raw_data_size = static_cast(replay_data.size()); - u8* rdbs = reinterpret_cast(&raw_data_size); - - // Add header and footer to replay file - std::vector header( - {'{', 'U', 3, 'r', 'a', 'w', '[', '$', 'U', '#', 'l', rdbs[3], rdbs[2], rdbs[1], rdbs[0]}); - replay_data.insert(replay_data.begin(), header.begin(), header.end()); - std::vector footer({'U', 8, 'm', 'e', 't', 'a', 'd', 'a', 't', 'a', '{', '}', '}'}); - replay_data.insert(replay_data.end(), footer.begin(), footer.end()); - - std::vector gzipped_data; - gzipped_data.resize(replay_data.size()); - auto res_size = compressToGzip(reinterpret_cast(&replay_data[0]), replay_data.size(), - reinterpret_cast(&gzipped_data[0]), gzipped_data.size()); - gzipped_data.resize(res_size); - - INFO_LOG_FMT(SLIPPI_ONLINE, "Pre-compression size: {}. Post compression size: {}", - replay_data.size(), res_size); - - curl_easy_setopt(m_curl_upload, CURLOPT_URL, url.c_str()); - curl_easy_setopt(m_curl_upload, CURLOPT_READDATA, &gzipped_data); - curl_easy_setopt(m_curl_upload, CURLOPT_INFILESIZE, res_size); - CURLcode res = curl_easy_perform(m_curl_upload); - - if (res != 0) - { - ERROR_LOG_FMT(SLIPPI_ONLINE, "[GameReport] Got error uploading replay file. Err code: {}", - static_cast(res)); - } -} diff --git a/Source/Core/Core/Slippi/SlippiGameReporter.h b/Source/Core/Core/Slippi/SlippiGameReporter.h deleted file mode 100644 index 11daa5867e..0000000000 --- a/Source/Core/Core/Slippi/SlippiGameReporter.h +++ /dev/null @@ -1,89 +0,0 @@ -#pragma once - -#include -#include // std::condition_variable -#include -#include -#include // std::mutex, std::unique_lock -#include -#include -#include -#include -#include "Common/CommonTypes.h" -#include "Core/Slippi/SlippiMatchmaking.h" -#include "Core/Slippi/SlippiUser.h" - -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; - int report_attempts = 0; - 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 = 0; - std::vector players; - }; - - SlippiGameReporter(SlippiUser* user, const std::string current_file_name); - ~SlippiGameReporter(); - - void StartReport(GameReport report); - void ReportAbandonment(std::string match_id); - void ReportCompletion(std::string matchId, u8 endMode); - void StartNewSession(); - void ReportThreadHandler(); - void PushReplayData(u8* data, u32 length, std::string action); - void UploadReplay(int idx, std::string url); - -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"; - const std::string COMPLETE_URL = "https://rankings-dot-slippi.uc.r.appspot.com/complete"; - CURL* m_curl = nullptr; - struct curl_slist* m_curl_header_list = nullptr; - - CURL* m_curl_upload = nullptr; - struct curl_slist* m_curl_upload_headers = nullptr; - - char m_curl_err_buf[CURL_ERROR_SIZE]; - char m_curl_upload_err_buf[CURL_ERROR_SIZE]; - - std::unordered_map known_desync_isos = { - {"23d6baef06bd65989585096915da20f2", true}, - {"27a5668769a54cd3515af47b8d9982f3", true}, - {"5805fa9f1407aedc8804d0472346fc5f", true}, - {"9bb3e275e77bb1a160276f2330f93931", true}, - }; - - SlippiUser* m_user; - std::string m_iso_hash; - std::queue game_report_queue; - std::thread reporting_thread; - std::mutex mtx; - std::condition_variable cv; - std::atomic run_thread; - std::thread m_md5_thread; - - std::map> m_replay_data; - int m_replay_write_idx = 0; - int m_replay_last_completed_idx = -1; -};