Merge pull request #72 from r2dliu/port-pr/245

A bunch of PRs for chat
This commit is contained in:
Nikhil Narayana 2022-05-08 23:47:27 -07:00 committed by GitHub
commit 1c714503d1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 11943 additions and 3665 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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -71,6 +71,7 @@
#define BACKUP_DIR "Backup"
#define RESOURCEPACK_DIR "ResourcePacks"
#define DYNAMICINPUT_DIR "DynamicInputTextures"
#define SLIPPI_DIR "Slippi"
// This one is only used to remove it if it was present
#define SHADERCACHE_LEGACY_DIR "ShaderCache"

View file

@ -913,6 +913,7 @@ static void RebuildUserDirectories(unsigned int dir_index)
s_user_paths[D_BACKUP_IDX] = s_user_paths[D_USER_IDX] + BACKUP_DIR DIR_SEP;
s_user_paths[D_RESOURCEPACK_IDX] = s_user_paths[D_USER_IDX] + RESOURCEPACK_DIR DIR_SEP;
s_user_paths[D_DYNAMICINPUT_IDX] = s_user_paths[D_LOAD_IDX] + DYNAMICINPUT_DIR DIR_SEP;
s_user_paths[D_SLIPPI_IDX] = s_user_paths[D_USER_IDX] + SLIPPI_DIR DIR_SEP;
s_user_paths[F_DOLPHINCONFIG_IDX] = s_user_paths[D_CONFIG_IDX] + DOLPHIN_CONFIG;
s_user_paths[F_GCPADCONFIG_IDX] = s_user_paths[D_CONFIG_IDX] + GCPAD_CONFIG;
s_user_paths[F_WIIPADCONFIG_IDX] = s_user_paths[D_CONFIG_IDX] + WIIPAD_CONFIG;
@ -930,6 +931,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

@ -55,6 +55,7 @@ enum
D_BACKUP_IDX,
D_RESOURCEPACK_IDX,
D_DYNAMICINPUT_IDX,
D_SLIPPI_IDX,
F_DOLPHINCONFIG_IDX,
F_GCPADCONFIG_IDX,
F_WIIPADCONFIG_IDX,
@ -72,6 +73,7 @@ enum
F_MEMORYWATCHERSOCKET_IDX,
F_WIISDCARD_IDX,
F_USERJSON_IDX,
F_DIRECTCODESJSON_IDX,
F_DUALSHOCKUDPCLIENTCONFIG_IDX,
NUM_PATH_INDICES
};

View file

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

View file

@ -160,7 +160,7 @@ const Info<bool> MAIN_NETWORK_SSL_DUMP_PEER_CERT{{System::Main, "Network", "SSLD
const Info<bool> MAIN_USE_HIGH_CONTRAST_TOOLTIPS{
{System::Main, "Interface", "UseHighContrastTooltips"}, true};
const Info<bool> MAIN_USE_PANIC_HANDLERS{{System::Main, "Interface", "UsePanicHandlers"}, true};
const Info<bool> MAIN_USE_PANIC_HANDLERS{{System::Main, "Interface", "UsePanicHandlers"}, false};
const Info<bool> MAIN_OSD_MESSAGES{{System::Main, "Interface", "OnScreenDisplayMessages"}, true};
const Info<bool> MAIN_SKIP_NKIT_WARNING{{System::Main, "Interface", "SkipNKitWarning"}, false};

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

@ -29,6 +29,7 @@
#include "Core/PowerPC/PowerPC.h"
#include "Core/Slippi/SlippiMatchmaking.h"
#include "Core/Slippi/SlippiPlayback.h"
#include "Core/Slippi/SlippiPremadeText.h"
#include "Core/Slippi/SlippiReplayComm.h"
#include "Core/State.h"
@ -120,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());
@ -1829,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;
@ -1873,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
@ -1925,7 +2059,7 @@ void CEXISlippi::prepareOnlineMatchState()
if (mmState == SlippiMatchmaking::ProcessState::CONNECTION_SUCCESS)
{
localPlayerIndex = matchmaking->LocalPlayerIndex();
m_local_player_index = matchmaking->LocalPlayerIndex();
if (!slippi_netplay)
{
@ -1972,14 +2106,14 @@ void CEXISlippi::prepareOnlineMatchState()
if (remotePlayerCount == 1)
{
auto isDecider = slippi_netplay->IsDecider();
localPlayerIndex = isDecider ? 0 : 1;
remotePlayerIndex = isDecider ? 1 : 0;
m_local_player_index = isDecider ? 0 : 1;
m_remote_player_index = isDecider ? 1 : 0;
}
#endif
auto isDecider = slippi_netplay->IsDecider();
localPlayerIndex = isDecider ? 0 : 1;
remotePlayerIndex = isDecider ? 1 : 0;
m_local_player_index = isDecider ? 0 : 1;
m_remote_player_index = isDecider ? 1 : 0;
}
else
{
@ -2022,7 +2156,7 @@ void CEXISlippi::prepareOnlineMatchState()
#ifdef LOCAL_TESTING
localPlayerIndex = 0;
chatMessageId = localChatMessageId;
sentChatMessageId = localChatMessageId;
chatMessagePlayerIdx = 0;
localChatMessageId = 0;
// in CSS p1 is always current player and p2 is opponent
@ -2030,18 +2164,39 @@ void CEXISlippi::prepareOnlineMatchState()
oppName = p2Name = "Player 2";
#endif
m_read_queue.push_back(localPlayerReady); // Local player ready
m_read_queue.push_back(remotePlayersReady); // Remote players ready
m_read_queue.push_back(localPlayerIndex); // Local player index
m_read_queue.push_back(remotePlayerIndex); // Remote player index
m_read_queue.push_back(localPlayerReady); // Local player ready
m_read_queue.push_back(remotePlayersReady); // Remote players ready
m_read_queue.push_back(m_local_player_index); // Local player index
m_read_queue.push_back(m_remote_player_index); // Remote player index
// Set chat message if any
if (slippi_netplay)
{
auto remoteMessageSelection = slippi_netplay->GetSlippiRemoteChatMessage();
chatMessageId = remoteMessageSelection.messageId;
chatMessagePlayerIdx = remoteMessageSelection.playerIdx;
auto isSingleMode = matchmaking && matchmaking->RemotePlayerCount() == 1;
sentChatMessageId = slippi_netplay->GetSlippiRemoteSentChatMessage();
// Prevent processing a message in the same frame
if (sentChatMessageId <= 0)
{
auto remoteMessageSelection = slippi_netplay->GetSlippiRemoteChatMessage();
chatMessageId = remoteMessageSelection.messageId;
chatMessagePlayerIdx = remoteMessageSelection.playerIdx;
if (chatMessageId == SlippiPremadeText::CHAT_MSG_CHAT_DISABLED && !isSingleMode)
{
// Clear remote chat messages if we are on teams and the player has chat disabled.
// Could also be handled on SlippiNetplay if the instance had acccess to the current
// connection mode
chatMessageId = chatMessagePlayerIdx = 0;
}
}
else
{
chatMessagePlayerIdx = m_local_player_index;
}
if (isSingleMode || !matchmaking)
{
chatMessagePlayerIdx = sentChatMessageId > 0 ? m_local_player_index : m_remote_player_index;
}
// in CSS p1 is always current player and p2 is opponent
localPlayerName = p1Name = userInfo.display_name;
}
@ -2111,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;
@ -2254,11 +2409,11 @@ void CEXISlippi::prepareOnlineMatchState()
}
// Create the opponent string using the names of all players on opposing teams
int teamIdx = onlineMatchBlock[0x69 + localPlayerIndex * 0x24];
int teamIdx = onlineMatchBlock[0x69 + m_local_player_index * 0x24];
std::string oppText = "";
for (int i = 0; i < 4; i++)
{
if (i == localPlayerIndex)
if (i == m_local_player_index)
continue;
if (onlineMatchBlock[0x69 + i * 0x24] != teamIdx)
@ -2270,7 +2425,7 @@ void CEXISlippi::prepareOnlineMatchState()
}
}
if (matchmaking->RemotePlayerCount() == 1)
oppText = matchmaking->GetPlayerName(remotePlayerIndex);
oppText = matchmaking->GetPlayerName(m_remote_player_index);
oppName = ConvertStringForGame(oppText, MAX_NAME_LENGTH * 2 + 1);
m_read_queue.insert(m_read_queue.end(), oppName.begin(), oppName.end());
@ -2407,6 +2562,70 @@ void CEXISlippi::prepareGctLoad(u8* payload)
m_read_queue.insert(m_read_queue.end(), gct.begin(), gct.end());
}
std::vector<u8> CEXISlippi::loadPremadeText(u8* payload)
{
u8 textId = payload[0];
std::vector<u8> premadeTextData;
auto spt = SlippiPremadeText();
if (textId >= SlippiPremadeText::SPT_CHAT_P1 && textId <= SlippiPremadeText::SPT_CHAT_P4)
{
auto port = textId - 1;
std::string playerName;
if (matchmaking)
playerName = matchmaking->GetPlayerName(port);
#ifdef LOCAL_TESTING
std::string defaultNames[] = {"Player 1", "lol u lost 2 dk", "Player 3", "Player 4"};
playerName = defaultNames[port];
#endif
u8 paramId = payload[1];
for (auto it = spt.unsupportedStringMap.begin(); it != spt.unsupportedStringMap.end(); it++)
{
playerName = ReplaceAll(playerName.c_str(), it->second, ""); // Remove unsupported chars
playerName = ReplaceAll(playerName.c_str(), it->first,
it->second); // Remap delimiters for premade text
}
// Replaces spaces with premade text space
playerName = ReplaceAll(playerName.c_str(), " ", "<S>");
if (paramId == SlippiPremadeText::CHAT_MSG_CHAT_DISABLED)
{
return premadeTextData =
spt.GetPremadeTextData(SlippiPremadeText::SPT_CHAT_DISABLED, playerName.c_str());
}
auto chatMessage = spt.premadeTextsParams.at(paramId);
std::string param = ReplaceAll(chatMessage.c_str(), " ", "<S>");
premadeTextData = spt.GetPremadeTextData(textId, playerName.c_str(), param.c_str());
}
else
{
premadeTextData = spt.GetPremadeTextData(textId);
}
return premadeTextData;
}
void CEXISlippi::preparePremadeTextLength(u8* payload)
{
std::vector<u8> premadeTextData = loadPremadeText(payload);
m_read_queue.clear();
// Write size to output
appendWordToBuffer(&m_read_queue, static_cast<u32>(premadeTextData.size()));
}
void CEXISlippi::preparePremadeTextLoad(u8* payload)
{
std::vector<u8> premadeTextData = loadPremadeText(payload);
m_read_queue.clear();
// Write data to output
m_read_queue.insert(m_read_queue.end(), premadeTextData.begin(), premadeTextData.end());
}
void CEXISlippi::handleChatMessage(u8* payload)
{
int messageId = payload[0];
@ -2694,9 +2913,18 @@ 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;
case CMD_PREMADE_TEXT_LENGTH:
preparePremadeTextLength(&memPtr[bufLoc + 1]);
break;
case CMD_PREMADE_TEXT_LOAD:
preparePremadeTextLoad(&memPtr[bufLoc + 1]);
break;
case CMD_OPEN_LOGIN:
handleLogInRequest();
break;
@ -2747,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,
@ -82,6 +84,8 @@ private:
CMD_GCT_LENGTH = 0xD3,
CMD_GCT_LOAD = 0xD4,
CMD_GET_DELAY = 0xD5,
CMD_PREMADE_TEXT_LENGTH = 0xE1,
CMD_PREMADE_TEXT_LOAD = 0xE2,
};
enum
@ -122,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
@ -130,6 +135,8 @@ private:
{CMD_GCT_LENGTH, 0x0},
{CMD_GCT_LOAD, 0x4},
{CMD_GET_DELAY, 0x0},
{CMD_PREMADE_TEXT_LENGTH, 0x2},
{CMD_PREMADE_TEXT_LOAD, 0x2},
};
struct WriteMessage
@ -170,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);
@ -198,6 +207,12 @@ private:
void prepareGctLength();
void prepareGctLoad(u8* payload);
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);
void FileWriteThread(void);
@ -233,8 +248,8 @@ private:
// We put these at the class level to preserve values in the case of a disconnect
// while loading. Without this, someone could load into a game playing the wrong char
u8 localPlayerIndex = 0;
u8 remotePlayerIndex = 1;
u8 m_local_player_index = 0;
u8 m_remote_player_index = 1;
// Frame skipping variables
int framesToSkip = 0;
@ -251,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,238 @@
#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::CreateFullPath(directCodesFilePath) && 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;
#else
std::string directCodesPath = File::GetUserPath(D_SLIPPI_IDX) + m_fileName;
#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

@ -56,7 +56,7 @@ void SlippiMatchmaking::FindMatch(MatchSearchSettings settings)
{
isMmConnected = false;
ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Starting matchmaking...");
ERROR_LOG_FMT(SLIPPI_ONLINE, "[Matchmaking] Starting matchmaking...");
m_searchSettings = settings;
@ -129,7 +129,6 @@ int SlippiMatchmaking::receiveMessage(json& msg, int timeoutMs)
netEvent.packet->data + netEvent.packet->dataLength);
std::string str(buf.begin(), buf.end());
INFO_LOG(SLIPPI_ONLINE, "[Matchmaking] Received: %s", str.c_str());
msg = json::parse(str);
enet_packet_destroy(netEvent.packet);
@ -222,8 +221,8 @@ void SlippiMatchmaking::startMatchmaking()
if (userInfo.port > 0)
m_hostPort = userInfo.port;
else
m_hostPort = 49000 + (generator() % 2000);
ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Port to use: %d...", m_hostPort);
m_hostPort = 41000 + (generator() % 10000);
ERROR_LOG_FMT(SLIPPI_ONLINE, "[Matchmaking] Port to use: {}...", m_hostPort);
// We are explicitly setting the client address because we are trying to utilize our connection
// to the matchmaking service in order to hole punch. This port will end up being the port
@ -241,7 +240,7 @@ void SlippiMatchmaking::startMatchmaking()
// Failed to create client
m_state = ProcessState::ERROR_ENCOUNTERED;
m_errorMsg = "Failed to create mm client";
ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Failed to create client...");
ERROR_LOG_FMT(SLIPPI_ONLINE, "[Matchmaking] Failed to create client...");
return;
}
@ -256,7 +255,7 @@ void SlippiMatchmaking::startMatchmaking()
// Failed to connect to server
m_state = ProcessState::ERROR_ENCOUNTERED;
m_errorMsg = "Failed to start connection to mm server";
ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Failed to start connection to mm server...");
ERROR_LOG_FMT(SLIPPI_ONLINE, "[Matchmaking] Failed to start connection to mm server...");
return;
}
@ -272,7 +271,7 @@ void SlippiMatchmaking::startMatchmaking()
connectAttemptCount++;
if (connectAttemptCount >= 20)
{
ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Failed to connect to mm server...");
ERROR_LOG_FMT(SLIPPI_ONLINE, "[Matchmaking] Failed to connect to mm server...");
m_state = ProcessState::ERROR_ENCOUNTERED;
m_errorMsg = "Failed to connect to mm server";
return;
@ -284,43 +283,84 @@ void SlippiMatchmaking::startMatchmaking()
netEvent.peer->data = &userInfo.display_name;
m_client->intercept = ENetUtil::InterceptCallback;
isMmConnected = true;
ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Connected to mm server...");
ERROR_LOG_FMT(SLIPPI_ONLINE, "[Matchmaking] Connected to mm server...");
}
ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Trying to find match...");
ERROR_LOG_FMT(SLIPPI_ONLINE, "[Matchmaking] Trying to find match...");
// if (!m_user->IsLoggedIn())
// {
// ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Must be logged in to queue");
// m_state = ProcessState::ERROR_ENCOUNTERED;
// m_errorMsg = "Must be logged in to queue. Go back to menu";
// return;
// }
/*if (!m_user->IsLoggedIn())
{
ERROR_LOG_FMT(SLIPPI_ONLINE, "[Matchmaking] Must be logged in to queue");
m_state = ProcessState::ERROR_ENCOUNTERED;
m_errorMsg = "Must be logged in to queue. Go back to menu";
return;
}*/
// The following code attempts to fetch the LAN IP such that when remote IPs match, the
// LAN IP can be tried in order to establish a connection in the case where the players
// don't have NAT loopback which allows that type of connection.
// Right now though, the logic would replace the WAN IP with the LAN IP and if the LAN
// IP connection didn't work but WAN would have, the players can no longer connect.
// Two things need to happen to improtve this logic:
// 1. The connection must be changed to try both the LAN and WAN IPs in the case of
// matching WAN IPs
// 2. The process for fetching LAN IP must be improved. For me, the current method
// would always fetch my VirtualBox IP, which is not correct. I also think perhaps
// it didn't work on Linux/Mac but I haven't tested it.
// I left this logic on for now under the assumption that it will help more people than
// it will hurt
char lan_addr[30]{};
// Compute LAN IP, in case 2 people are connecting from one IP we can send them each other's local
// IP instead of public. Experimental to allow people from behind one router to connect.
char host[256];
char lan_addr[30];
char* ip;
char ip[INET_ADDRSTRLEN]{};
struct hostent* host_entry;
int hostname;
hostname = gethostname(host, sizeof(host)); // find the host name
// attempt at using getaddrinfo, only ever sees 127.0.0.1 for some reason. for now the existing
// impl will work because we only use ipv4.
// struct addrinfo hints = {0}, *addrs;
// hints.ai_family = AF_INET;
// const int status = getaddrinfo(nullptr, std::to_string(m_hostPort).c_str(), &hints, &addrs);
// if (status != 0)
// {
// ERROR_LOG_FMT(SLIPPI_ONLINE, "[Matchmaking] Error finding LAN address");
// return;
// }
// for (struct addrinfo* addrinfo = addrs; addrinfo != nullptr; addrinfo = addrinfo->ai_next)
// {
// if (getnameinfo(addrs->ai_addr, static_cast<socklen_t>(addrs->ai_addrlen), lan_addr,
// sizeof(lan_addr), nullptr, 0, NI_NUMERICHOST))
// {
// continue;
// }
// WARN_LOG_FMT(SLIPPI_ONLINE, "[Matchmaking] IP via getaddrinfo {}", lan_addr);
// }
// freeaddrinfo(addrs);
if (hostname == -1)
{
ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Error finding LAN address");
ERROR_LOG_FMT(SLIPPI_ONLINE, "[Matchmaking] Error finding LAN address");
}
else
{
host_entry = gethostbyname(host); // find host information
if (host_entry == NULL)
if (host_entry == NULL || host_entry->h_addrtype != AF_INET)
{
ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Error finding LAN host");
ERROR_LOG_FMT(SLIPPI_ONLINE, "[Matchmaking] Error finding LAN host");
}
else
{
ip = inet_ntoa(*((struct in_addr*)host_entry->h_addr_list[0])); // Convert into IP string
INFO_LOG(SLIPPI_ONLINE, "[Matchmaking] LAN IP: %s", ip);
// Fetch the last IP (because that was correct for me, not sure if it will be for all)
int i = 0;
while (host_entry->h_addr_list[i] != 0)
{
inet_ntop(AF_INET, host_entry->h_addr_list[i], ip, INET_ADDRSTRLEN);
WARN_LOG_FMT(SLIPPI_ONLINE, "[Matchmaking] IP at idx {}: {}", i, ip);
i++;
}
sprintf(lan_addr, "%s:%d", ip, m_hostPort);
WARN_LOG_FMT(SLIPPI_ONLINE, "[Matchmaking] Sending LAN address: {}", lan_addr);
}
}
@ -342,8 +382,8 @@ void SlippiMatchmaking::startMatchmaking()
int rcvRes = receiveMessage(response, 5000);
if (rcvRes != 0)
{
ERROR_LOG(SLIPPI_ONLINE,
"[Matchmaking] Did not receive response from server for create ticket");
ERROR_LOG_FMT(SLIPPI_ONLINE,
"[Matchmaking] Did not receive response from server for create ticket");
m_state = ProcessState::ERROR_ENCOUNTERED;
m_errorMsg = "Failed to join mm queue";
return;
@ -352,8 +392,8 @@ void SlippiMatchmaking::startMatchmaking()
std::string respType = response["type"];
if (respType != MmMessageType::CREATE_TICKET_RESP)
{
ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Received incorrect response for create ticket");
ERROR_LOG(SLIPPI_ONLINE, "%s", response.dump().c_str());
ERROR_LOG_FMT(SLIPPI_ONLINE, "[Matchmaking] Received incorrect response for create ticket");
ERROR_LOG_FMT(SLIPPI_ONLINE, "{}", response.dump().c_str());
m_state = ProcessState::ERROR_ENCOUNTERED;
m_errorMsg = "Invalid response when joining mm queue";
return;
@ -362,14 +402,14 @@ void SlippiMatchmaking::startMatchmaking()
std::string err = response.value("error", "");
if (err.length() > 0)
{
ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Received error from server for create ticket");
ERROR_LOG_FMT(SLIPPI_ONLINE, "[Matchmaking] Received error from server for create ticket");
m_state = ProcessState::ERROR_ENCOUNTERED;
m_errorMsg = err;
return;
}
m_state = ProcessState::MATCHMAKING;
ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Request ticket success");
ERROR_LOG_FMT(SLIPPI_ONLINE, "[Matchmaking] Request ticket success");
}
void SlippiMatchmaking::handleMatchmaking()
@ -383,13 +423,13 @@ void SlippiMatchmaking::handleMatchmaking()
int rcvRes = receiveMessage(getResp, 2000);
if (rcvRes == -1)
{
INFO_LOG(SLIPPI_ONLINE, "[Matchmaking] Have not yet received assignment");
INFO_LOG_FMT(SLIPPI_ONLINE, "[Matchmaking] Have not yet received assignment");
return;
}
else if (rcvRes != 0)
{
// Right now the only other code is -2 meaning the server died probably?
ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Lost connection to the mm server");
ERROR_LOG_FMT(SLIPPI_ONLINE, "[Matchmaking] Lost connection to the mm server");
m_state = ProcessState::ERROR_ENCOUNTERED;
m_errorMsg = "Lost connection to the mm server";
return;
@ -398,7 +438,7 @@ void SlippiMatchmaking::handleMatchmaking()
std::string respType = getResp["type"];
if (respType != MmMessageType::GET_TICKET_RESP)
{
ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Received incorrect response for get ticket");
ERROR_LOG_FMT(SLIPPI_ONLINE, "[Matchmaking] Received incorrect response for get ticket");
m_state = ProcessState::ERROR_ENCOUNTERED;
m_errorMsg = "Invalid response when getting mm status";
return;
@ -410,12 +450,12 @@ void SlippiMatchmaking::handleMatchmaking()
{
if (latestVersion != "")
{
// Update file to get new version number when the mm server tells us our version is outdated
// Update version number when the mm server tells us our version is outdated
m_user->OverwriteLatestVersion(
latestVersion); // Force latest version for people whose file updates dont work
}
ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Received error from server for get ticket");
ERROR_LOG_FMT(SLIPPI_ONLINE, "[Matchmaking] Received error from server for get ticket");
m_state = ProcessState::ERROR_ENCOUNTERED;
m_errorMsg = err;
return;
@ -517,8 +557,8 @@ void SlippiMatchmaking::handleMatchmaking()
terminateMmConnection();
m_state = ProcessState::OPPONENT_CONNECTING;
ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Opponent found. isDecider: %s",
m_isHost ? "true" : "false");
ERROR_LOG_FMT(SLIPPI_ONLINE, "[Matchmaking] Opponent found. isDecider: {}",
m_isHost ? "true" : "false");
}
int SlippiMatchmaking::LocalPlayerIndex()
@ -578,6 +618,7 @@ void SlippiMatchmaking::handleConnecting()
{
ipLog << m_remoteIps[i] << ", ";
}
// INFO_LOG_FMT(SLIPPI_ONLINE, "[Matchmaking] My port: {} || {}", m_hostPort, ipLog.str());
// Is host is now used to specify who the decider is
auto client = std::make_unique<SlippiNetplayClient>(addrs, ports, remotePlayerCount, m_hostPort,
@ -588,7 +629,7 @@ void SlippiMatchmaking::handleConnecting()
auto status = client->GetSlippiConnectStatus();
if (status == SlippiNetplayClient::SlippiConnectStatus::NET_CONNECT_STATUS_INITIATED)
{
INFO_LOG(SLIPPI_ONLINE, "[Matchmaking] Connection not yet successful");
INFO_LOG_FMT(SLIPPI_ONLINE, "[Matchmaking] Connection not yet successful");
Common::SleepCurrentThread(500);
// Deal with class shut down
@ -602,7 +643,7 @@ void SlippiMatchmaking::handleConnecting()
{
// If we failed setting up a connection in teams mode, show a detailed error about who we had
// issues connecting to.
ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Failed to connect to players");
ERROR_LOG_FMT(SLIPPI_ONLINE, "[Matchmaking] Failed to connect to players");
m_state = ProcessState::ERROR_ENCOUNTERED;
m_errorMsg = "Timed out waiting for other players to connect";
auto failedConns = client->GetFailedConnections();
@ -625,8 +666,8 @@ void SlippiMatchmaking::handleConnecting()
}
else if (status != SlippiNetplayClient::SlippiConnectStatus::NET_CONNECT_STATUS_CONNECTED)
{
ERROR_LOG(SLIPPI_ONLINE,
"[Matchmaking] Connection attempt failed, looking for someone else.");
ERROR_LOG_FMT(SLIPPI_ONLINE,
"[Matchmaking] Connection attempt failed, looking for someone else.");
// Return to the start to get a new ticket to find someone else we can hopefully connect with
m_netplayClient = nullptr;
@ -634,7 +675,7 @@ void SlippiMatchmaking::handleConnecting()
return;
}
ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Connection success!");
ERROR_LOG_FMT(SLIPPI_ONLINE, "[Matchmaking] Connection success!");
// Successful connection
m_netplayClient = std::move(client);

View file

@ -61,8 +61,8 @@ public:
static bool IsFixedRulesMode(OnlinePlayMode mode);
protected:
const std::string MM_HOST_DEV = "35.197.121.196"; // Dev host
const std::string MM_HOST_PROD = "35.247.98.48"; // Production host
const std::string MM_HOST_DEV = "mm2.slippi.gg"; // Dev host
const std::string MM_HOST_PROD = "mm.slippi.gg"; // Production host
const u16 MM_PORT = 43113;
std::string MM_HOST = "";

View file

@ -13,6 +13,7 @@
#include "Core/ConfigManager.h"
#include "Core/Core.h"
#include "Core/NetPlayProto.h"
#include "SlippiPremadeText.h"
#include "VideoCommon/OnScreenDisplay.h"
#include "VideoCommon/VideoConfig.h"
@ -358,8 +359,18 @@ unsigned int SlippiNetplayClient::OnData(sf::Packet& packet, ENetPeer* peer)
case NetPlay::NP_MSG_SLIPPI_CHAT_MESSAGE:
{
auto playerSelection = ReadChatMessageFromPacket(packet);
INFO_LOG(SLIPPI_ONLINE, "[Netplay] Received chat message from opponent %d: %d",
playerSelection->playerIdx, playerSelection->messageId);
INFO_LOG_FMT(SLIPPI_ONLINE, "[Netplay] Received chat message from opponent {}: {}",
playerSelection->playerIdx, playerSelection->messageId);
// if chat is not enabled, automatically send back a message saying so
if (!SConfig::GetInstance().m_slippiEnableQuickChat)
{
auto chat_packet = std::make_unique<sf::Packet>();
remoteSentChatMessageId = SlippiPremadeText::CHAT_MSG_CHAT_DISABLED;
WriteChatMessageToPacket(*chat_packet, remoteSentChatMessageId, LocalPlayerPort());
SendAsync(std::move(chat_packet));
remoteSentChatMessageId = 0;
break;
}
// set message id to netplay instance
remoteChatMessageSelection = std::move(playerSelection);
}

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

@ -0,0 +1,429 @@
#pragma once
// This file must be encoded in UTF-8 with signatures on Windows
#include <regex>
#include <stdarg.h>
#include <string>
#include <unordered_map>
#include <vector>
#include "Common/CommonTypes.h"
#include "Common/Logging/Log.h"
#include "Common/StringUtil.h"
class SlippiPremadeText
{
public:
enum
{
SPT_CHAT_P1 = 0x1,
SPT_CHAT_P2 = 0x2,
SPT_CHAT_P3 = 0x3,
SPT_CHAT_P4 = 0x4,
SPT_LOGOUT = 0x5,
SPT_CHAT_DISABLED = 0x6,
CHAT_MSG_U_PAD_LEFT = 0x81,
CHAT_MSG_U_PAD_RIGHT = 0x82,
CHAT_MSG_U_PAD_DOWN = 0x84,
CHAT_MSG_U_PAD_UP = 0x88,
CHAT_MSG_L_PAD_LEFT = 0x11,
CHAT_MSG_L_PAD_RIGHT = 0x12,
CHAT_MSG_L_PAD_DOWN = 0x14,
CHAT_MSG_L_PAD_UP = 0x18,
CHAT_MSG_R_PAD_LEFT = 0x21,
CHAT_MSG_R_PAD_RIGHT = 0x22,
CHAT_MSG_R_PAD_DOWN = 0x24,
CHAT_MSG_R_PAD_UP = 0x28,
CHAT_MSG_D_PAD_LEFT = 0x41,
CHAT_MSG_D_PAD_RIGHT = 0x42,
CHAT_MSG_D_PAD_DOWN = 0x44,
CHAT_MSG_D_PAD_UP = 0x48,
CHAT_MSG_CHAT_DISABLED = 0x10,
};
const std::unordered_map<u8, std::string> premadeTextsParams = {
{CHAT_MSG_U_PAD_UP, "ggs"},
{CHAT_MSG_U_PAD_LEFT, "one more"},
{CHAT_MSG_U_PAD_RIGHT, "brb"},
{CHAT_MSG_U_PAD_DOWN, "good luck"},
{CHAT_MSG_L_PAD_UP, "well played"},
{CHAT_MSG_L_PAD_LEFT, "that was fun"},
{CHAT_MSG_L_PAD_RIGHT, "thanks"},
{CHAT_MSG_L_PAD_DOWN, "too good"},
{CHAT_MSG_R_PAD_UP, "oof"},
{CHAT_MSG_R_PAD_LEFT, "my b"},
{CHAT_MSG_R_PAD_RIGHT, "lol"},
{CHAT_MSG_R_PAD_DOWN, "wow"},
{CHAT_MSG_D_PAD_UP, "okay"},
{CHAT_MSG_D_PAD_LEFT, "thinking"},
{CHAT_MSG_D_PAD_RIGHT, "lets play again later"},
{CHAT_MSG_D_PAD_DOWN, "bad connection"},
{CHAT_MSG_CHAT_DISABLED, "player has chat disabled"},
};
const std::unordered_map<u8, std::string> premadeTexts = {
{SPT_CHAT_P1, "<LEFT><KERN><COLOR, 229, 76, 76>%s:<S><COLOR, 255, 255, 255>%s<END>"},
{SPT_CHAT_P2, "<LEFT><KERN><COLOR, 59, 189, 255>%s:<S><COLOR, 255, 255, 255>%s<END>"},
{SPT_CHAT_P3, "<LEFT><KERN><COLOR, 255, 203, 4>%s:<S><COLOR, 255, 255, 255>%s<END>"},
{SPT_CHAT_P4, "<LEFT><KERN><COLOR, 0, 178, 2>%s:<S><COLOR, 255, 255, 255>%s<END>"},
{SPT_LOGOUT, "<FIT><COLOR, 243, 75, 75>Are<S>You<COLOR, 0, 175, 75><S>Sure?<END>"},
{SPT_CHAT_DISABLED,
"<LEFT><KERN><COLOR, 0, 178, 2>%s<S><COLOR, 255, 255, 255>has<S>chat<S>disabled<S><END>"},
};
// This is just a map of delimiters and temporary replacements to remap them before the name is
// converted to Slippi Premade Text format
const std::unordered_map<std::string, std::string> unsupportedStringMap = {
{"<", "\\"}, {">", "`"}, {",", ""}, // DELETE U+007F
};
// TODO: use va_list to handle any no. or args
std::string GetPremadeTextString(u8 textId) { return premadeTexts.at(textId); }
std::vector<u8> GetPremadeTextData(u8 textId, ...)
{
std::string format = GetPremadeTextString(textId);
char str[400];
va_list args;
va_start(args, textId);
vsprintf(str, format.c_str(), args);
va_end(args);
// INFO_LOG(SLIPPI, "%s", str);
std::vector<u8> data = {};
std::vector<u8> empty = {};
std::vector<std::string> matches = std::vector<std::string>();
// NOTE: This code is converted from HSDRaw C# code
// Fuck Regex, current cpp version does not support positive lookaheads to match this pattern
// "((?<=<).+?(?=>))|((?<=>*)([^>]+?)(?=<) Good ol' fashioned nested loop :)
auto splitted = split(str, ">");
for (int i = 0; i < splitted.size(); i++)
{
auto splitted2 = split(splitted[i], "<");
for (int j = 0; j < splitted2.size(); j++)
{
if (splitted2[j].length() > 0)
matches.push_back(splitted2[j]);
}
}
std::string match;
for (int m = 0; m < matches.size(); m++)
{
match = matches[m];
auto splittedMatches = split(match, ",");
if (splittedMatches.size() == 0)
continue;
std::string firstMatch = splittedMatches[0];
auto utfMatch = UTF8ToUTF32(firstMatch);
std::pair<TEXT_OP_CODE, std::pair<std::string, std::string>> key = findCodeKey(firstMatch);
if (key.first != TEXT_OP_CODE::CUSTOM_NULL)
{
if (splittedMatches.size() - 1 != strlen(key.second.second.c_str()))
return empty;
data.push_back((u8)key.first);
std::string res;
std::string res2;
for (int j = 0; j < strlen(key.second.second.c_str()); j++)
{
switch (key.second.second.c_str()[j])
{
case 'b':
res = splittedMatches[j + 1];
trim(res);
if (static_cast<u8>(atoi(res.c_str())))
data.push_back(static_cast<u8>(atoi(res.c_str())));
else
data.push_back(0);
break;
case 's':
res2 = splittedMatches[j + 1];
trim(res2);
u16 sht = static_cast<u16>(atoi(res2.c_str()));
if (sht)
{
data.push_back(static_cast<u8>(sht >> 8));
data.push_back(static_cast<u8>(sht & 0xFF));
}
else
{
data.push_back(0);
data.push_back(0);
}
break;
}
}
}
else
{
// process std::string otherwise
if (splittedMatches.size() >= 2 && firstMatch == "CHR")
{
std::string res3 = splittedMatches[1];
trim(res3);
u16 ch = static_cast<u16>(atoi(res3.c_str()));
if (ch)
{
u16 sht = (static_cast<u16>(TEXT_OP_CODE::SPECIAL_CHARACTER) << 8) | ch;
u8 r = static_cast<u8>(sht >> 8);
u8 r2 = static_cast<u8>(sht & 0xFF);
data.push_back(r);
data.push_back(r2);
}
}
else
{
for (unsigned long c = 0; c < utfMatch.length(); c++)
{
int chr = utfMatch[c];
// We are manually replacing "<" for "\" and ">" for "`" because I don't want to handle
// vargs and we need to prevent "format injection" lol...
for (auto it = unsupportedStringMap.begin(); it != unsupportedStringMap.end(); it++)
{
if (it->second.find(chr) != std::string::npos || (chr == U'Ç' && it->first[0] == ','))
{ // Need to figure out how to find extended ascii chars (Ç)
chr = it->first[0];
}
}
// Yup, fuck strchr and cpp too, I'm not in the mood to spend 4 more hours researching
// how to get Japanese characters properly working with a map, so I put everything on an
// int array in hex
int pos = -1;
for (int ccc = 0; ccc < 287; ccc++)
{
if (static_cast<int>(CHAR_MAP[ccc]) == chr)
{
pos = ccc;
break;
}
}
if (pos >= 0)
{
u16 sht = static_cast<u16>((TEXT_OP_CODE::COMMON_CHARACTER << 8) | pos);
u8 r = static_cast<u8>(sht >> 8);
u8 r2 = static_cast<u8>(sht & 0xFF);
// INFO_LOG(SLIPPI, "%x %x %x %c", sht, r, r2, chr);
data.push_back(r);
data.push_back(r2);
}
// otherwise ignore
}
}
}
}
data.push_back(0x00); // Always add end, just in case
return data;
}
private:
enum TEXT_OP_CODE
{
END = 0x00,
RESET = 0x01,
UNKNOWN_02 = 0x02,
LINE_BREAK = 0x03,
UNKNOWN_04 = 0x04,
UNKNOWN_05 = 0x05,
UNKNOWN_06 = 0x06,
OFFSET = 0x07,
UNKNOWN_08 = 0x08,
UNKNOWN_09 = 0x09,
SCALING = 0x0A,
RESET_SCALING = 0x0B,
COLOR = 0x0C,
CLEAR_COLOR = 0x0D,
SET_TEXTBOX = 0x0E,
RESET_TEXTBOX = 0x0F,
CENTERED = 0x10,
RESET_CENTERED = 0x11,
LEFT_ALIGNED = 0x12,
RESET_LEFT_ALIGN = 0x13,
RIGHT_ALIGNED = 0x14,
RESET_RIGHT_ALIGN = 0x15,
KERNING = 0x16,
NO_KERNING = 0x17,
FITTING = 0x18,
NO_FITTING = 0x19,
SPACE = 0x1A,
COMMON_CHARACTER = 0x20,
SPECIAL_CHARACTER = 0x40,
CUSTOM_NULL = 0x99,
};
std::vector<std::tuple<TEXT_OP_CODE, std::vector<u16>>> OPCODES;
const std::unordered_map<TEXT_OP_CODE, std::pair<std::string, std::string>> CODES = {
{TEXT_OP_CODE::CENTERED, std::pair<std::string, std::string>("CENTER", "")},
{TEXT_OP_CODE::RESET_CENTERED, std::pair<std::string, std::string>("/CENTER", "")},
{TEXT_OP_CODE::CLEAR_COLOR, std::pair<std::string, std::string>("/COLOR", "")},
{TEXT_OP_CODE::COLOR, std::pair<std::string, std::string>("COLOR", "bbb")},
{TEXT_OP_CODE::END, std::pair<std::string, std::string>("END", "")},
{TEXT_OP_CODE::FITTING, std::pair<std::string, std::string>("FIT", "")},
{TEXT_OP_CODE::KERNING, std::pair<std::string, std::string>("KERN", "")},
{TEXT_OP_CODE::LEFT_ALIGNED, std::pair<std::string, std::string>("LEFT", "")},
{TEXT_OP_CODE::LINE_BREAK, std::pair<std::string, std::string>("BR", "")},
{TEXT_OP_CODE::NO_FITTING, std::pair<std::string, std::string>("/FIT", "")},
{TEXT_OP_CODE::NO_KERNING, std::pair<std::string, std::string>("/KERN", "")},
{TEXT_OP_CODE::OFFSET, std::pair<std::string, std::string>("OFFSET", "ss")},
{TEXT_OP_CODE::RESET, std::pair<std::string, std::string>("RESET", "")},
{TEXT_OP_CODE::RESET_LEFT_ALIGN, std::pair<std::string, std::string>("/LEFT", "")},
{TEXT_OP_CODE::RESET_RIGHT_ALIGN, std::pair<std::string, std::string>("/RIGHT", "")},
{TEXT_OP_CODE::RESET_SCALING, std::pair<std::string, std::string>("/SCALE", "")},
{TEXT_OP_CODE::RESET_TEXTBOX, std::pair<std::string, std::string>("/TEXTBOX", "")},
{TEXT_OP_CODE::RIGHT_ALIGNED, std::pair<std::string, std::string>("/RIGHT", "")},
{TEXT_OP_CODE::SCALING, std::pair<std::string, std::string>("SCALE", "bbbb")},
{TEXT_OP_CODE::SET_TEXTBOX, std::pair<std::string, std::string>("TEXTBOX", "ss")},
{TEXT_OP_CODE::UNKNOWN_02, std::pair<std::string, std::string>("UNK02", "")},
{TEXT_OP_CODE::UNKNOWN_04, std::pair<std::string, std::string>("UNK04", "")},
{TEXT_OP_CODE::UNKNOWN_05, std::pair<std::string, std::string>("UNK05", "s")},
{TEXT_OP_CODE::UNKNOWN_06, std::pair<std::string, std::string>("UNK06", "ss")},
{TEXT_OP_CODE::UNKNOWN_08, std::pair<std::string, std::string>("UNK08", "")},
{TEXT_OP_CODE::UNKNOWN_09, std::pair<std::string, std::string>("UNK09", "")},
{TEXT_OP_CODE::SPACE, std::pair<std::string, std::string>("S", "")},
};
std::pair<TEXT_OP_CODE, std::pair<std::string, std::string>> findCodeKey(std::string p)
{
for (auto it = CODES.begin(); it != CODES.end(); it++)
{
if (it->second.first == p)
{
return *it;
}
}
return std::pair<TEXT_OP_CODE, std::pair<std::string, std::string>>(
TEXT_OP_CODE::CUSTOM_NULL, std::pair<std::string, std::string>("", ""));
}
std::vector<std::tuple<TEXT_OP_CODE, std::vector<u16>>> DeserializeCodes(std::vector<u8> data)
{
std::vector<std::tuple<TEXT_OP_CODE, std::vector<u16>>> d =
std::vector<std::tuple<TEXT_OP_CODE, std::vector<u16>>>();
for (int i = 0; i < data.size();)
{
auto opcode = (TEXT_OP_CODE)data[i++];
std::vector<u16> param = std::vector<u16>(0);
u8 textCode = static_cast<u8>(opcode);
if ((textCode >> 4) == 2 || (textCode >> 4) == 4)
param = std::vector<u16>{static_cast<u16>(((textCode << 8) | (data[i++] & 0xFF)) & 0xFFF)};
else if (!CODES.count(opcode))
{
ERROR_LOG_FMT(SLIPPI, "Opcode Not Supported!");
}
else
{
std::pair<std::string, std::string> code = CODES.at(opcode);
auto p = code.second.c_str();
param = std::vector<u16>(strlen(p));
for (int j = 0; j < param.size(); j++)
{
switch (p[j])
{
case 'b':
param[j] = static_cast<u16>(data[i++] & 0xFF);
break;
case 's':
param[j] = static_cast<u16>(((data[i++] & 0xFF) << 8) | (data[i++] & 0xFF));
break;
}
}
}
std::pair<TEXT_OP_CODE, std::vector<u16>> c =
std::pair<TEXT_OP_CODE, std::vector<u16>>(opcode, param);
d.push_back(c);
if (opcode == TEXT_OP_CODE::END)
break;
}
return d;
}
// https://stackoverflow.com/questions/216823/whats-the-best-way-to-trim-stdstring
// trim from start (in place)
static inline void ltrim(std::string& s)
{
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](int ch) { return !std::isspace(ch); }));
}
// trim from end (in place)
static inline void rtrim(std::string& s)
{
s.erase(std::find_if(s.rbegin(), s.rend(), [](int ch) { return !std::isspace(ch); }).base(),
s.end());
}
// trim from both ends (in place)
static inline void trim(std::string& s)
{
ltrim(s);
rtrim(s);
}
std::vector<std::string> split(const std::string& str, const std::string& delim)
{
std::vector<std::string> tokens;
size_t prev = 0, pos = 0;
do
{
pos = str.find(delim, prev);
if (pos == std::string::npos)
pos = str.length();
std::string token = str.substr(prev, pos - prev);
if (!token.empty())
tokens.push_back(token);
prev = pos + delim.length();
} while (pos < str.length() && prev < str.length());
return tokens;
}
// region CharMAPS
const int CHAR_MAP[287] = {
U'0', U'1', U'2', U'3', U'4', U'5', U'6', U'7', U'8', U'9', U'A', U'B', U'C',
U'D', U'E', U'F', U'G', U'H', U'I', U'J', U'K', U'L', U'M', U'N', U'O', U'P',
U'Q', U'R', U'S', U'T', U'U', U'V', U'W', U'X', U'Y', U'Z', U'a', U'b', U'c',
U'd', U'e', U'f', U'g', U'h', U'i', U'j', U'k', U'l', U'm', U'n', U'o', U'p',
U'q', U'r', U's', U't', U'u', U'v', U'w', U'x', U'y', U'z', U'', U'', U'',
U'', U'', U'', U'', U'', U'', U'', U'', U'', U'', U'', U'', U'',
U'', U'', U'', U'', U'', U'', U'', U'', U'', U'', U'', U'', U'',
U'', U'', U'', U'', U'', U'', U'', U'', U'', U'', U'', U'', U'',
U'', U'', U'', U'', U'', U'', U'', U'', U'', U'', U'', U'', U'',
U'', U'', U'', U'', U'', U'', U'', U'', U'', U'', U'', U'', U'',
U'', U'', U'', U'', U'', U'', U'', U'', U'', U'', U'', U'', U'',
U'', U'', U'', U'', U'', U'', U'', U'', U'', U'', U'', U'', U'',
U'', U'', U'', U'', U'', U'', U'', U'', U'', U'', U'', U'', U'',
U'', U'', U'', U'', U'', U'', U'', U'', U'', U'', U'', U'', U'',
U'', U'', U'', U'', U'', U'', U'', U'', U'', U'', U'', U'', U'',
U'', U'', U'', U'', U'', U'', U'', U'', U'', U'', U'', U'', U'',
U'', U'', U'', U'', U'', U'', U'', U'', U'', U'', U'', U'', U'',
U'', U'', U'', U'', U'', U'', U' ', U'', U'', U',', U'.', U'', U':',
U';', U'?', U'!', U'^', U'_', U'', U'/', U'~', U'|', U'\'', U'"', U'(', U')',
U'[', U']', U'{', U'}', U'+', '-', U'×', U'=', U'<', U'>', U'¥', U'$', U'%',
U'#', U'&', U'*', U'@', U'', U'', U'', U'', U'', U'', U'', U'', U'',
U'', U'', U'', U'', U'', U'', U'', U'', U'', U'', U'', U'', U'',
U''};
};