compiles, working, playable matches. connect code history feature works

This commit is contained in:
R2DLiu 2021-12-19 20:06:33 -05:00 committed by Nikhil Narayana
commit de49277949
13 changed files with 8894 additions and 1437 deletions

Binary file not shown.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -930,6 +930,7 @@ static void RebuildUserDirectories(unsigned int dir_index)
s_user_paths[F_GCSRAM_IDX] = s_user_paths[D_GCUSER_IDX] + GC_SRAM;
s_user_paths[F_WIISDCARD_IDX] = s_user_paths[D_WIIROOT_IDX] + DIR_SEP WII_SDCARD;
s_user_paths[F_USERJSON_IDX] = s_user_paths[D_USER_IDX] + "user.json";
s_user_paths[F_DIRECTCODESJSON_IDX] = s_user_paths[D_USER_IDX] + "directcodes.json";
s_user_paths[D_MEMORYWATCHER_IDX] = s_user_paths[D_USER_IDX] + MEMORYWATCHER_DIR DIR_SEP;
s_user_paths[F_MEMORYWATCHERLOCATIONS_IDX] =

View file

@ -72,6 +72,7 @@ enum
F_MEMORYWATCHERSOCKET_IDX,
F_WIISDCARD_IDX,
F_USERJSON_IDX,
F_DIRECTCODESJSON_IDX,
F_DUALSHOCKUDPCLIENTCONFIG_IDX,
NUM_PATH_INDICES
};

View file

@ -488,9 +488,10 @@ add_library(core
Slippi/SlippiSpectate.h
Slippi/SlippiUser.cpp
Slippi/SlippiUser.h
Slippi/SlippiGameReporter.cpp
Slippi/SlippiGameReporter.h
Slippi/SlippiPremadeText.h
Slippi/SlippiGameReporter.cpp
Slippi/SlippiGameReporter.h
Slippi/SlippiDirectCodes.cpp
Slippi/SlippiPremadeText.h
)
if(_M_X86)

View file

@ -65,7 +65,7 @@ public:
// For savestates. storing it here seemed cleaner than requiring each implementation to report its
// type. I know this class is set up like an interface, but no code requires it to be strictly
// such.
TEXIDevices m_device_type;
TEXIDevices m_device_type{EXIDEVICE_NONE};
private:
// Byte transfer function for this device

View file

@ -121,6 +121,8 @@ CEXISlippi::CEXISlippi()
gameFileLoader = std::make_unique<SlippiGameFileLoader>();
game_reporter = std::make_unique<SlippiGameReporter>(user.get());
g_replayComm = std::make_unique<SlippiReplayComm>();
directCodes = std::make_unique<SlippiDirectCodes>("direct-codes.json");
teamsCodes = std::make_unique<SlippiDirectCodes>("teams-codes.json");
generator = std::default_random_engine(Common::Timer::GetTimeMs());
@ -1830,6 +1832,20 @@ void CEXISlippi::startFindMatch(u8* payload)
shiftJisCode.insert(shiftJisCode.begin(), &payload[1], &payload[1] + 18);
shiftJisCode.erase(std::find(shiftJisCode.begin(), shiftJisCode.end(), 0x00), shiftJisCode.end());
// Log the direct code to file.
if (search.mode == SlippiMatchmaking::DIRECT)
{
// Make sure to convert to UTF8, otherwise json library will fail when
// calling dump().
std::string utf8Code = SHIFTJISToUTF8(shiftJisCode);
directCodes->AddOrUpdateCode(utf8Code);
}
else if (search.mode == SlippiMatchmaking::TEAMS)
{
std::string utf8Code = SHIFTJISToUTF8(shiftJisCode);
teamsCodes->AddOrUpdateCode(utf8Code);
}
// TODO: Make this work so we dont have to pass shiftJis to mm server
// search.connectCode = SHIFTJISToUTF8(shiftJisCode).c_str();
search.connectCode = shiftJisCode;
@ -1874,6 +1890,123 @@ void CEXISlippi::startFindMatch(u8* payload)
#endif
}
bool CEXISlippi::doesTagMatchInput(u8* input, u8 inputLen, std::string tag)
{
auto jisTag = UTF8ToSHIFTJIS(tag);
// Check if this tag matches what has been input so far
bool isMatch = true;
for (int i = 0; i < inputLen; i++)
{
// ERROR_LOG(SLIPPI_ONLINE, "Entered: %X%X. History: %X%X", input[i * 3], input[i * 3 + 1],
// (u8)jisTag[i * 2],
// (u8)jisTag[i * 2 + 1]);
if (input[i * 3] != (u8)jisTag[i * 2] || input[i * 3 + 1] != (u8)jisTag[i * 2 + 1])
{
isMatch = false;
break;
}
}
return isMatch;
}
void CEXISlippi::handleNameEntryLoad(u8* payload)
{
u8 inputLen = payload[24];
u32 initialIndex = payload[25] << 24 | payload[26] << 16 | payload[27] << 8 | payload[28];
u8 scrollDirection = payload[29];
u8 curMode = payload[30];
auto codeHistory = directCodes.get();
if (curMode == SlippiMatchmaking::TEAMS)
{
codeHistory = teamsCodes.get();
}
// Adjust index
u32 curIndex = initialIndex;
if (scrollDirection == 1)
{
curIndex++;
}
else if (scrollDirection == 2)
{
curIndex = curIndex > 0 ? curIndex - 1 : curIndex;
}
else if (scrollDirection == 3)
{
curIndex = 0;
}
// Scroll to next tag that
std::string tagAtIndex = "1";
while (curIndex >= 0 && curIndex < (u32)codeHistory->length())
{
tagAtIndex = codeHistory->get(curIndex);
// Break if we have found a tag that matches
if (doesTagMatchInput(payload, inputLen, tagAtIndex))
break;
curIndex = scrollDirection == 2 ? curIndex - 1 : curIndex + 1;
}
INFO_LOG(SLIPPI_ONLINE, "Idx: %d, InitIdx: %d, Scroll: %d. Len: %d", curIndex, initialIndex,
scrollDirection, inputLen);
tagAtIndex = codeHistory->get(curIndex);
if (tagAtIndex == "1")
{
// If we failed to find a tag at the current index, try the initial index again.
// If the initial index matches the filter, preserve that suggestion. Without
// this logic, the suggestion would get cleared
auto initialTag = codeHistory->get(initialIndex);
if (doesTagMatchInput(payload, inputLen, initialTag))
{
tagAtIndex = initialTag;
curIndex = initialIndex;
}
}
INFO_LOG(SLIPPI_ONLINE, "Retrieved tag: %s", tagAtIndex.c_str());
std::string jisCode;
m_read_queue.clear();
if (tagAtIndex == "1")
{
m_read_queue.push_back(0);
m_read_queue.insert(m_read_queue.end(), payload, payload + 3 * inputLen);
m_read_queue.insert(m_read_queue.end(), 3 * (8 - inputLen), 0);
m_read_queue.push_back(inputLen);
appendWordToBuffer(&m_read_queue, initialIndex);
return;
}
// Indicate we have a suggestion
m_read_queue.push_back(1);
// Convert to tag to shift jis and write to response
jisCode = UTF8ToSHIFTJIS(tagAtIndex);
// Write out connect code into buffer, injection null terminator after each letter
for (int i = 0; i < 8; i++)
{
for (int j = i * 2; j < i * 2 + 2; j++)
{
m_read_queue.push_back(j < jisCode.length() ? jisCode[j] : 0);
}
m_read_queue.push_back(0x0);
}
INFO_LOG(SLIPPI_ONLINE, "New Idx: %d. Jis Code length: %d", curIndex, (u8)(jisCode.length() / 2));
// Write length of tag
m_read_queue.push_back(static_cast<u8>(jisCode.length() / 2));
appendWordToBuffer(&m_read_queue, curIndex);
}
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
@ -2133,7 +2266,7 @@ void CEXISlippi::prepareOnlineMatchState()
// Overwrite stage information. Make sure everyone loads the same stage
u16 stageId = 0x1F; // Default to battlefield if there was no selection
for (auto selections : orderedSelections)
for (const auto& selections : orderedSelections)
{
if (!selections.isStageSelected)
continue;
@ -2780,6 +2913,9 @@ void CEXISlippi::DMAWrite(u32 _uAddr, u32 _uSize)
case CMD_FILE_LENGTH:
prepareFileLength(&memPtr[bufLoc + 1]);
break;
case CMD_FETCH_CODE_SUGGESTION:
handleNameEntryLoad(&memPtr[bufLoc + 1]);
break;
case CMD_FILE_LOAD:
prepareFileLoad(&memPtr[bufLoc + 1]);
break;
@ -2839,7 +2975,7 @@ void CEXISlippi::DMARead(u32 addr, u32 size)
{
if (m_read_queue.empty())
{
INFO_LOG(EXPANSIONINTERFACE, "EXI SLIPPI DMARead: Empty");
ERROR_LOG(SLIPPI, "EXI SLIPPI DMARead: Empty");
return;
}

View file

@ -9,6 +9,7 @@
#include "Common/CommonTypes.h"
#include "Common/File.h"
#include "Common/FileUtil.h"
#include "Core/Slippi/SlippiDirectCodes.h"
#include "Core/Slippi/SlippiGameFileLoader.h"
#include "Core/Slippi/SlippiGameReporter.h"
#include "Core/Slippi/SlippiMatchmaking.h"
@ -74,6 +75,7 @@ private:
CMD_SEND_CHAT_MESSAGE = 0xBB,
CMD_GET_NEW_SEED = 0xBC,
CMD_REPORT_GAME = 0xBD,
CMD_FETCH_CODE_SUGGESTION = 0xBE,
// Misc
CMD_LOG_MESSAGE = 0xD0,
@ -124,6 +126,7 @@ private:
{CMD_CLEANUP_CONNECTION, 0},
{CMD_GET_NEW_SEED, 0},
{CMD_REPORT_GAME, 16},
{CMD_FETCH_CODE_SUGGESTION, 31},
// Misc
{CMD_LOG_MESSAGE, 0xFFFF}, // Variable size... will only work if by itself
@ -174,6 +177,8 @@ private:
void handleSendInputs(u8* payload);
void handleCaptureSavestate(u8* payload);
void handleLoadSavestate(u8* payload);
void handleNameEntryAutoComplete(u8* payload);
void handleNameEntryLoad(u8* payload);
void startFindMatch(u8* payload);
void prepareOnlineMatchState();
void setMatchSelections(u8* payload);
@ -204,6 +209,8 @@ private:
void prepareDelayResponse();
void preparePremadeTextLength(u8* payload);
void preparePremadeTextLoad(u8* payload);
bool doesTagMatchInput(u8* input, u8 inputLen, std::string tag);
std::vector<u8> loadPremadeText(u8* payload);
int getCharColor(u8 charId, u8 teamId);
@ -259,6 +266,8 @@ private:
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;
std::map<s32, std::unique_ptr<SlippiSavestate>> activeSavestates;
std::deque<std::unique_ptr<SlippiSavestate>> availableSavestates;

View file

@ -0,0 +1,241 @@
#include "SlippiDirectCodes.h"
#ifdef _WIN32
#include "AtlBase.h"
#include "AtlConv.h"
#endif
#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 <codecvt>
#include <locale>
#include <time.h>
#include <json.hpp>
using json = nlohmann::json;
SlippiDirectCodes::SlippiDirectCodes(std::string fileName)
{
m_fileName = fileName;
// Prevent additional file reads, if we've already loaded data to memory.
// if (directCodeInfos.empty())
ReadFile();
Sort();
}
SlippiDirectCodes::~SlippiDirectCodes()
{
// Add additional cleanup behavior here? Just added something
// So compiler wouldn't nag.
return;
}
void SlippiDirectCodes::ReadFile()
{
std::string directCodesFilePath = getCodesFilePath();
INFO_LOG(SLIPPI_ONLINE, "Looking for direct codes file at %s", directCodesFilePath.c_str());
if (!File::Exists(directCodesFilePath))
{
// Attempt to create empty file with array as parent json item.
if (File::CreateEmptyFile(directCodesFilePath))
{
File::WriteStringToFile("[\n]", directCodesFilePath);
}
else
{
WARN_LOG(SLIPPI_ONLINE, "Was unable to create %s", directCodesFilePath.c_str());
}
}
std::string directCodesFileContents;
File::ReadFileToString(directCodesFilePath, directCodesFileContents);
directCodeInfos = parseFile(directCodesFileContents);
}
void SlippiDirectCodes::AddOrUpdateCode(std::string code)
{
WARN_LOG(SLIPPI_ONLINE, "Attempting to add or update direct code: %s", code.c_str());
time_t curTime;
time(&curTime);
u8 dateTimeStrLength = sizeof "20171015T095717";
std::vector<char> dateTimeBuf(dateTimeStrLength);
strftime(&dateTimeBuf[0], dateTimeStrLength, "%Y%m%dT%H%M%S", localtime(&curTime));
std::string timestamp(&dateTimeBuf[0]);
bool found = false;
for (auto it = directCodeInfos.begin(); it != directCodeInfos.end(); ++it)
{
if (it->connectCode == code)
{
found = true;
it->lastPlayed = timestamp;
}
}
if (!found)
{
CodeInfo newDirectCode = {code, timestamp, false};
directCodeInfos.push_back(newDirectCode);
}
// TODO: Maybe remove from here?
// Or start a thread that is periodically called, if file writes will happen enough.
WriteFile();
}
void SlippiDirectCodes::Sort(u8 sortByProperty)
{
switch (sortByProperty)
{
case SORT_BY_TIME:
std::sort(
directCodeInfos.begin(), directCodeInfos.end(),
[](const CodeInfo a, const CodeInfo b) -> bool { return a.lastPlayed > b.lastPlayed; });
break;
case SORT_BY_NAME:
std::sort(
directCodeInfos.begin(), directCodeInfos.end(),
[](const CodeInfo a, const CodeInfo b) -> bool { return a.connectCode < b.connectCode; });
break;
}
}
std::string SlippiDirectCodes::Autocomplete(std::string startText)
{
// Pre-sort direct codes.
Sort();
// Find first entry in our sorted vector that starts with the given text.
for (auto it = directCodeInfos.begin(); it != directCodeInfos.end(); it++)
{
if (it->connectCode.rfind(startText, 0) == 0)
{
return it->connectCode;
}
}
return startText;
}
std::string SlippiDirectCodes::get(int index)
{
Sort();
if (index < directCodeInfos.size() && index >= 0)
{
return directCodeInfos.at(index).connectCode;
}
INFO_LOG(SLIPPI_ONLINE, "Out of bounds name entry index %d", index);
return (index >= directCodeInfos.size()) ? "1" : "";
}
int SlippiDirectCodes::length()
{
return (int)directCodeInfos.size();
}
void SlippiDirectCodes::WriteFile()
{
std::string directCodesFilePath = getCodesFilePath();
// Outer empty array.
json fileData = json::array();
// Inner contents.
json directCodeData = json::object();
// TODO Define constants for string literals.
for (auto it = directCodeInfos.begin(); it != directCodeInfos.end(); ++it)
{
directCodeData["connectCode"] = it->connectCode;
directCodeData["lastPlayed"] = it->lastPlayed;
directCodeData["isFavorite"] = it->isFavorite;
fileData.emplace_back(directCodeData);
}
File::WriteStringToFile(fileData.dump(), directCodesFilePath);
}
std::string SlippiDirectCodes::getCodesFilePath()
{
std::string fileName = m_fileName + ".json";
// TODO: Move to User dir
#if defined(__APPLE__)
std::string directCodesPath =
File::GetBundleDirectory() + "/Contents/Resources" + DIR_SEP + m_fileName;
#elif defined(_WIN32)
std::string directCodesPath = File::GetExeDirectory() + DIR_SEP + m_fileName;
#else
std::string directCodesPath = File::GetUserPath(D_USER_IDX) + m_fileName;
// directCodesPath.pop_back();
#endif
return directCodesPath;
}
inline std::string readString(json obj, std::string key)
{
auto item = obj.find(key);
if (item == obj.end() || item.value().is_null())
{
return "";
}
return obj[key];
}
inline bool readBool(json obj, std::string key)
{
auto item = obj.find(key);
if (item == obj.end() || item.value().is_null())
{
return false;
}
return obj[key];
}
std::vector<SlippiDirectCodes::CodeInfo> SlippiDirectCodes::parseFile(std::string fileContents)
{
std::vector<SlippiDirectCodes::CodeInfo> directCodes;
json res = json::parse(fileContents, nullptr, false);
// Unlike the user.json, the encapsulating type should be an array.
if (res.is_discarded() || !res.is_array())
{
WARN_LOG(SLIPPI_ONLINE, "Malformed json in direct codes file.");
return directCodes;
}
// Retrieve all saved direct codes and related info
for (auto it = res.begin(); it != res.end(); ++it)
{
if (it.value().is_object())
{
CodeInfo curDirectCode;
curDirectCode.connectCode = readString(*it, "connectCode");
curDirectCode.lastPlayed = readString(*it, "lastPlayed");
curDirectCode.isFavorite = readBool(*it, "favorite");
directCodes.push_back(curDirectCode);
}
}
return directCodes;
}

View file

@ -0,0 +1,39 @@
#pragma once
#include <atomic>
#include <string>
#include <thread>
#include <vector>
#include "Common/CommonTypes.h"
class SlippiDirectCodes
{
public:
static const uint8_t SORT_BY_TIME = 1;
static const uint8_t SORT_BY_FAVORITE = 2;
static const uint8_t SORT_BY_NAME = 3;
struct CodeInfo
{
std::string connectCode = "";
std::string lastPlayed = "";
bool isFavorite = false;
};
SlippiDirectCodes(std::string fileName);
~SlippiDirectCodes();
void ReadFile();
void AddOrUpdateCode(std::string code);
std::string get(int index);
int length();
void Sort(u8 sortByProperty = SlippiDirectCodes::SORT_BY_TIME);
std::string Autocomplete(std::string startText);
protected:
void WriteFile();
std::string getCodesFilePath();
std::vector<CodeInfo> parseFile(std::string fileContents);
std::vector<CodeInfo> directCodeInfos;
std::string m_fileName;
};

View file

@ -42,19 +42,19 @@ struct SlippiRemotePadOutput
class SlippiPlayerSelections
{
public:
u8 playerIdx = 0;
u8 characterId = 0;
u8 characterColor = 0;
u8 teamId = 0;
u8 playerIdx{};
u8 characterId{};
u8 characterColor{};
u8 teamId{};
bool isCharacterSelected = false;
u16 stageId = 0;
u16 stageId{};
bool isStageSelected = false;
u32 rngOffset = 0;
u32 rngOffset{};
int messageId;
int messageId{};
void Merge(SlippiPlayerSelections& s)
{

View file

@ -269,7 +269,6 @@ private:
COMMON_CHARACTER = 0x20,
SPECIAL_CHARACTER = 0x40,
CUSTOM_NULL = 0x99,
};
std::vector<std::tuple<TEXT_OP_CODE, std::vector<u16>>> OPCODES;