Rust Game Reporter (deaddda)

This commit is contained in:
Nikhil Narayana 2023-08-13 17:50:24 -07:00
commit 85f5914c5b
No known key found for this signature in database
GPG key ID: 1B34839FA8D6245E
9 changed files with 144 additions and 658 deletions

View file

@ -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,

View file

@ -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"};

View file

@ -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

View file

@ -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

View file

@ -146,7 +146,6 @@ CEXISlippi::CEXISlippi(Core::System& system, const std::string current_file_name
g_playbackStatus = std::make_unique<SlippiPlaybackStatus>();
matchmaking = std::make_unique<SlippiMatchmaking>(user.get());
gameFileLoader = std::make_unique<SlippiGameFileLoader>();
game_reporter = std::make_unique<SlippiGameReporter>(user.get(), current_file_name);
g_replayComm = std::make_unique<SlippiReplayComm>();
directCodes = std::make_unique<SlippiDirectCodes>("direct-codes.json");
teamsCodes = std::make_unique<SlippiDirectCodes>("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<u8> CEXISlippi::generateMetadata()
// Add game start time
u8 dateTimeStrLength = sizeof "2011-10-08T07:07:09Z";
std::vector<char> 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<u8>(dateTimeBuf.size())});
@ -519,12 +520,12 @@ void CEXISlippi::writeToFile(std::unique_ptr<WriteMessage> 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<char> 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<char> 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<u32>(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<u8*>(&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<u8*>(&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<SlippiMatchmaking::OnlinePlayMode>(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<SlippiMatchmakingOnlinePlayMode>(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<u8>(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<u8>(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<std::vector<std::string>> messagesByPlayer = {
std::vector<std::vector<std::string>> 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
}

View file

@ -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<u8, std::unordered_map<u8, u32>> characterUsage;
@ -255,8 +254,8 @@ private:
std::vector<u8> m_read_queue;
std::unique_ptr<Slippi::SlippiGame> 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<u16> stagePool;
@ -299,7 +298,6 @@ private:
std::unique_ptr<SlippiGameFileLoader> gameFileLoader;
std::unique_ptr<SlippiNetplayClient> slippi_netplay;
std::unique_ptr<SlippiMatchmaking> matchmaking;
std::unique_ptr<SlippiGameReporter> game_reporter;
std::unique_ptr<SlippiDirectCodes> directCodes;
std::unique_ptr<SlippiDirectCodes> teamsCodes;

View file

@ -40,7 +40,7 @@ struct ReportGameQuery
struct ReportSetCompletionQuery
{
u8 command;
u8 endMode;
u8 end_mode;
};
struct GpCompleteStepQuery

View file

@ -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 <codecvt>
#include <locale>
#include <json.hpp>
#include <mbedtls/md.h>
#include <mbedtls/md5.h>
#include <zlib.h>
using json = nlohmann::json;
static std::array<u8, 16> 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<u8>* buf = (std::vector<u8>*)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<std::mutex> 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<u8>(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<u8>(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<u8>(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<u32>(replay_data.size());
u8* rdbs = reinterpret_cast<u8*>(&raw_data_size);
// Add header and footer to replay file
std::vector<u8> 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<u8> footer({'U', 8, 'm', 'e', 't', 'a', 'd', 'a', 't', 'a', '{', '}', '}'});
replay_data.insert(replay_data.end(), footer.begin(), footer.end());
std::vector<u8> gzipped_data;
gzipped_data.resize(replay_data.size());
auto res_size = compressToGzip(reinterpret_cast<char*>(&replay_data[0]), replay_data.size(),
reinterpret_cast<char*>(&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<int>(res));
}
}

View file

@ -1,89 +0,0 @@
#pragma once
#include <atomic>
#include <condition_variable> // std::condition_variable
#include <curl/curl.h>
#include <map>
#include <mutex> // std::mutex, std::unique_lock
#include <queue>
#include <string>
#include <thread>
#include <vector>
#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<PlayerReport> 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<std::string, bool> known_desync_isos = {
{"23d6baef06bd65989585096915da20f2", true},
{"27a5668769a54cd3515af47b8d9982f3", true},
{"5805fa9f1407aedc8804d0472346fc5f", true},
{"9bb3e275e77bb1a160276f2330f93931", true},
};
SlippiUser* m_user;
std::string m_iso_hash;
std::queue<GameReport> game_report_queue;
std::thread reporting_thread;
std::mutex mtx;
std::condition_variable cv;
std::atomic<bool> run_thread;
std::thread m_md5_thread;
std::map<int, std::vector<u8>> m_replay_data;
int m_replay_write_idx = 0;
int m_replay_last_completed_idx = -1;
};