refactor: migrate SlippiUser to be backed by a Rust layer (#6)

* fix: user folder path for macOS

* fix: slippi_rust_extensions -> slippi-rust-extensions when linking

* fix: set application support folder to dolphin-beta and fix log
This commit is contained in:
Nikhil Narayana 2023-09-20 19:46:34 -07:00 committed by GitHub
commit 03887849e5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 119 additions and 364 deletions

@ -1 +1 @@
Subproject commit af04c6c1878731da5ccc7e1742f355af54755bb5
Subproject commit c5643d5d2cb7073b52dcfbdb7836771b264bc33d

View file

@ -154,7 +154,7 @@ PUBLIC
fmt::fmt
${MBEDTLS_LIBRARIES}
minizip-ng
slippi_rust_extensions
slippi-rust-extensions
PRIVATE
${CURL_LIBRARIES}

View file

@ -754,6 +754,19 @@ std::string GetBundleDirectory()
return app_bundle_path;
}
std::string GetApplicationSupportDirectory()
{
std::string dir =
File::GetHomeDirectory() + "/Library/Application Support/com.project-slippi.dolphin-beta";
if (!CreateDir(dir))
{
ERROR_LOG_FMT(COMMON, "Unable to create Application Support directory: {}", dir);
}
return dir;
}
#endif
std::string GetExePath()

View file

@ -234,6 +234,7 @@ void SetSysDirectory(const std::string& path);
#ifdef __APPLE__
std::string GetBundleDirectory();
std::string GetApplicationSupportDirectory();
#endif
std::string GetExePath();

View file

@ -61,8 +61,7 @@ enum class LogType : int
SLIPPI,
SLIPPI_ONLINE,
SLIPPI_RUST_DEPENDENCIES,
SLIPPI_RUST_EXI,
SLIPPI_RUST_GAME_REPORTER,
SLIPPI_RUST_ONLINE,
SLIPPI_RUST_JUKEBOX,
SP1,
SYMBOLS,

View file

@ -166,9 +166,7 @@ LogManager::LogManager()
m_log[LogType::SLIPPI_ONLINE] = {"SLIPPI_ONLINE", "Slippi Online"};
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_ONLINE] = {"SLIPPI_RUST_ONLINE,", "[Rust] Slippi Online", 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

@ -141,9 +141,17 @@ CEXISlippi::CEXISlippi(Core::System& system, const std::string current_file_name
{
INFO_LOG_FMT(SLIPPI, "EXI SLIPPI Constructor called.");
slprs_exi_device_ptr = slprs_exi_device_create(current_file_name.c_str(), OSDMessageHandler);
std::string user_file_path = File::GetUserPath(F_USERJSON_IDX);
user = std::make_unique<SlippiUser>();
SlippiRustEXIConfig slprs_exi_config;
slprs_exi_config.iso_path = current_file_name.c_str();
slprs_exi_config.user_json_path = user_file_path.c_str();
slprs_exi_config.scm_slippi_semver_str = Common::GetSemVerStr().c_str();
slprs_exi_config.osd_add_msg_fn = OSDMessageHandler;
slprs_exi_device_ptr = slprs_exi_device_create(slprs_exi_config);
user = std::make_unique<SlippiUser>(slprs_exi_device_ptr);
g_playback_status = std::make_unique<SlippiPlaybackStatus>();
matchmaking = std::make_unique<SlippiMatchmaking>(user.get());
game_file_loader = std::make_unique<SlippiGameFileLoader>();
@ -287,9 +295,7 @@ 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);
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());
slprs_exi_device_report_match_abandonment(slprs_exi_device_ptr, active_match_id.c_str());
}
handleConnectionCleanup();
@ -2714,7 +2720,6 @@ void CEXISlippi::handleChatMessage(u8* payload)
if (slippi_netplay)
{
auto user_info = user->GetUserInfo();
auto packet = std::make_unique<sf::Packet>();
// OSD::AddMessage("[Me]: "+ msg, OSD::Duration::VERY_LONG, OSD::Color::YELLOW);
slippi_netplay->remote_sent_chat_message_id = message_id;
@ -3017,10 +3022,8 @@ void CEXISlippi::handleCompleteSet(const SlippiExiTypes::ReportSetCompletionQuer
if (last_match_id.find("mode.ranked") != std::string::npos)
{
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(),
slprs_exi_device_report_match_completion(slprs_exi_device_ptr, last_match_id.c_str(),
query.end_mode);
}
}
@ -3031,12 +3034,10 @@ void CEXISlippi::handleGetPlayerSettings()
SlippiExiTypes::GetPlayerSettingsResponse resp = {};
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};
std::vector<std::vector<std::string>> messages_by_player = {{}, {}, {}, {}};
// These chat messages will be used when previewing messages
auto user_chat_messages = user->GetUserInfo().chat_messages;
auto user_chat_messages = user->GetUserChatMessages();
if (user_chat_messages.size() == 16)
{
messages_by_player[0] = user_chat_messages;
@ -3051,6 +3052,13 @@ void CEXISlippi::handleGetPlayerSettings()
for (int i = 0; i < 4; i++)
{
// If any of the users in the chat messages vector have a payload that is incorrect,
// force that player to the default chat messages. A valid payload is 16 entries.
if (messages_by_player[i].size() != 16)
{
messages_by_player[i] = user->GetDefaultChatMessages();
}
for (int j = 0; j < 16; j++)
{
auto str = ConvertStringForGame(messages_by_player[i][j], MAX_MESSAGE_LENGTH);

View file

@ -540,15 +540,19 @@ void SlippiMatchmaking::handleMatchmaking()
player_info.display_name = el.value("displayName", "");
player_info.connect_code = el.value("connectCode", "");
player_info.port = el.value("port", 0);
player_info.chat_messages = SlippiUser::default_chat_messages;
if (el["chatMessages"].is_array())
{
player_info.chat_messages = el.value("chatMessages", SlippiUser::default_chat_messages);
player_info.chat_messages = el.value("chatMessages", m_user->GetDefaultChatMessages());
if (player_info.chat_messages.size() != 16)
{
player_info.chat_messages = SlippiUser::default_chat_messages;
player_info.chat_messages = m_user->GetDefaultChatMessages();
}
}
else
{
player_info.chat_messages = m_user->GetDefaultChatMessages();
}
m_player_info.push_back(player_info);
if (is_local)

View file

@ -1,363 +1,94 @@
#include "SlippiUser.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 "Common/Version.h"
#include "Common/Common.h"
#include "Core/ConfigManager.h"
#include "SlippiRustExtensions.h"
#include <codecvt>
#include <locale>
#include <json.hpp>
using json = nlohmann::json;
const std::vector<std::string> SlippiUser::default_chat_messages = {
"ggs",
"one more",
"brb",
"good luck",
"well played",
"that was fun",
"thanks",
"too good",
"sorry",
"my b",
"lol",
"wow",
"gotta go",
"one sec",
"let's play again later",
"bad connection",
};
#ifdef _WIN32
#define MAX_SYSTEM_PROGRAM (4096)
static void system_hidden(const char* cmd)
// Takes a RustChatMessages pointer and extracts messages from them, then
// frees the underlying memory safely.
std::vector<std::string> ConvertChatMessagesFromRust(RustChatMessages* rsMessages)
{
PROCESS_INFORMATION p_info;
STARTUPINFO s_info;
std::vector<std::string> chatMessages;
memset(&s_info, 0, sizeof(s_info));
memset(&p_info, 0, sizeof(p_info));
s_info.cb = sizeof(s_info);
wchar_t utf16cmd[MAX_SYSTEM_PROGRAM] = {0};
MultiByteToWideChar(CP_UTF8, 0, cmd, -1, utf16cmd, MAX_SYSTEM_PROGRAM);
if (CreateProcessW(NULL, utf16cmd, NULL, NULL, 0, CREATE_NO_WINDOW, NULL, NULL, &s_info, &p_info))
for (int i = 0; i < rsMessages->len; i++)
{
DWORD ExitCode;
WaitForSingleObject(p_info.hProcess, INFINITE);
GetExitCodeProcess(p_info.hProcess, &ExitCode);
CloseHandle(p_info.hProcess);
CloseHandle(p_info.hThread);
std::string message = std::string(rsMessages->data[i]);
chatMessages.push_back(message);
}
}
#endif
static void RunSystemCommand(const std::string& command)
{
#ifdef _WIN32
_wsystem(UTF8ToTStr(command).c_str());
#else
system(command.c_str());
#endif
slprs_user_free_messages(rsMessages);
return chatMessages;
}
static size_t receive(char* ptr, size_t size, size_t nmemb, void* rcvBuf)
SlippiUser::SlippiUser(uintptr_t rs_exi_device_ptr)
{
size_t len = size * nmemb;
INFO_LOG_FMT(SLIPPI_ONLINE, "[User] Received data: {}", len);
std::string* buf = (std::string*)rcvBuf;
buf->insert(buf->end(), ptr, ptr + len);
return len;
}
SlippiUser::SlippiUser()
{
CURL* curl = curl_easy_init();
if (curl)
{
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &receive);
curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 5000);
// 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);
#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;
}
slprs_exi_device_ptr = rs_exi_device_ptr;
}
SlippiUser::~SlippiUser()
{
// Wait for thread to terminate
m_run_thread = false;
if (m_file_listen_thread.joinable())
m_file_listen_thread.join();
if (m_curl)
{
curl_slist_free_all(m_curl_header_list);
curl_easy_cleanup(m_curl);
}
// Do nothing, the exi ptr is cleaned up by the exi device
}
bool SlippiUser::AttemptLogin()
{
std::string user_file_path = getUserFilePath();
// TODO: Remove a couple updates after ranked
#ifndef __APPLE__
{
#ifdef _WIN32
std::string old_user_file_path = File::GetExeDirectory() + DIR_SEP + "user.json";
#else
std::string old_user_file_path = File::GetUserPath(D_USER_IDX) + DIR_SEP + "user.json";
#endif
if (File::Exists(old_user_file_path) && !File::Rename(old_user_file_path, user_file_path))
{
WARN_LOG_FMT(SLIPPI_ONLINE, "Could not move file {} to {}", old_user_file_path,
user_file_path);
}
}
#endif
// Get user file
std::string user_file_contents;
File::ReadFileToString(user_file_path, user_file_contents);
m_user_info = parseFile(user_file_contents);
m_is_logged_in = !m_user_info.uid.empty();
if (m_is_logged_in)
{
overwriteFromServer();
WARN_LOG_FMT(SLIPPI_ONLINE, "Found user {} ({})", m_user_info.display_name, m_user_info.uid);
}
return m_is_logged_in;
return slprs_user_attempt_login(slprs_exi_device_ptr);
}
void SlippiUser::OpenLogInPage()
{
std::string url = "https://slippi.gg/online/enable";
std::string path = getUserFilePath();
#ifdef _WIN32
// On windows, sometimes the path can have backslashes and slashes mixed, convert all to
// backslashes
path = ReplaceAll(path, "\\", "\\");
path = ReplaceAll(path, "/", "\\");
#endif
#ifndef __APPLE__
char* escaped_path = curl_easy_escape(nullptr, path.c_str(), static_cast<int>(path.length()));
path = std::string(escaped_path);
curl_free(escaped_path);
#endif
std::string full_url = url + "?path=" + path;
INFO_LOG_FMT(SLIPPI_ONLINE, "[User] Login at path: {}", full_url);
#ifdef _WIN32
std::string command = "explorer \"" + full_url + "\"";
#elif defined(__APPLE__)
std::string command = "open \"" + full_url + "\"";
#else
std::string command = "xdg-open \"" + full_url + "\""; // Linux
#endif
RunSystemCommand(command);
}
void SlippiUser::UpdateApp()
{
std::string url = "https://slippi.gg/downloads?update=true";
#ifdef _WIN32
std::string command = "explorer \"" + url + "\"";
#elif defined(__APPLE__)
std::string command = "open \"" + url + "\"";
#else
std::string command = "xdg-open \"" + url + "\""; // Linux
#endif
RunSystemCommand(command);
slprs_user_open_login_page(slprs_exi_device_ptr);
}
void SlippiUser::ListenForLogIn()
{
if (m_run_thread)
return;
slprs_user_listen_for_login(slprs_exi_device_ptr);
}
if (m_file_listen_thread.joinable())
m_file_listen_thread.join();
m_run_thread = true;
m_file_listen_thread = std::thread(&SlippiUser::FileListenThread, this);
bool SlippiUser::UpdateApp()
{
return slprs_user_update_app(slprs_exi_device_ptr);
}
void SlippiUser::LogOut()
{
m_run_thread = false;
deleteFile();
UserInfo empty_user;
m_is_logged_in = false;
m_user_info = empty_user;
slprs_user_logout(slprs_exi_device_ptr);
}
void SlippiUser::OverwriteLatestVersion(std::string version)
{
m_user_info.latest_version = version;
slprs_user_overwrite_latest_version(slprs_exi_device_ptr, version.c_str());
}
SlippiUser::UserInfo SlippiUser::GetUserInfo()
{
return m_user_info;
SlippiUser::UserInfo userInfo;
RustUserInfo* info = slprs_user_get_info(slprs_exi_device_ptr);
userInfo.uid = std::string(info->uid);
userInfo.play_key = std::string(info->play_key);
userInfo.display_name = std::string(info->display_name);
userInfo.connect_code = std::string(info->connect_code);
userInfo.latest_version = std::string(info->latest_version);
slprs_user_free_info(info);
return userInfo;
}
std::vector<std::string> SlippiUser::GetDefaultChatMessages()
{
RustChatMessages* chatMessages = slprs_user_get_default_messages(slprs_exi_device_ptr);
return ConvertChatMessagesFromRust(chatMessages);
}
std::vector<std::string> SlippiUser::GetUserChatMessages()
{
RustChatMessages* chatMessages = slprs_user_get_messages(slprs_exi_device_ptr);
return ConvertChatMessagesFromRust(chatMessages);
}
bool SlippiUser::IsLoggedIn()
{
return m_is_logged_in;
}
void SlippiUser::FileListenThread()
{
while (m_run_thread)
{
if (AttemptLogin())
{
m_run_thread = false;
break;
}
Common::SleepCurrentThread(500);
}
}
// On Linux platforms, the user.json file lives in the XDG_CONFIG_HOME/SlippiOnline
// directory in order to deal with the fact that we want the configuration for AppImage
// builds to be mutable.
std::string SlippiUser::getUserFilePath()
{
#if defined(__APPLE__)
std::string user_file_path =
File::GetBundleDirectory() + "/Contents/Resources" + DIR_SEP + "user.json";
#else
std::string user_file_path = File::GetUserPath(F_USERJSON_IDX);
INFO_LOG_FMT(SLIPPI, "{}", user_file_path);
#endif
return user_file_path;
}
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];
}
SlippiUser::UserInfo SlippiUser::parseFile(std::string file_contents)
{
UserInfo info;
info.file_contents = file_contents;
auto res = json::parse(file_contents, nullptr, false);
if (res.is_discarded() || !res.is_object())
{
return info;
}
info.uid = readString(res, "uid");
info.display_name = readString(res, "displayName");
info.play_key = readString(res, "playKey");
info.connect_code = readString(res, "connectCode");
info.latest_version = readString(res, "latestVersion");
info.chat_messages = SlippiUser::default_chat_messages;
if (res["chatMessages"].is_array())
{
info.chat_messages = res.value("chatMessages", SlippiUser::default_chat_messages);
if (info.chat_messages.size() != 16)
{
info.chat_messages = SlippiUser::default_chat_messages;
}
}
return info;
}
void SlippiUser::deleteFile()
{
std::string user_file_path = getUserFilePath();
File::Delete(user_file_path);
}
void SlippiUser::overwriteFromServer()
{
if (!m_curl)
return;
// Perform curl request
std::string resp;
curl_easy_setopt(m_curl, CURLOPT_URL,
(URL_START + "/" + m_user_info.uid + "?additionalFields=chatMessages").c_str());
curl_easy_setopt(m_curl, CURLOPT_WRITEDATA, &resp);
CURLcode res = curl_easy_perform(m_curl);
if (res != 0)
{
ERROR_LOG_FMT(SLIPPI, "[User] Error fetching user info from server, code: {}",
static_cast<u8>(res));
return;
}
long response_code;
curl_easy_getinfo(m_curl, CURLINFO_RESPONSE_CODE, &response_code);
if (response_code != 200)
{
ERROR_LOG_FMT(SLIPPI, "[User] Server responded with non-success status: {}", response_code);
return;
}
// Overwrite user info with data from server
auto r = json::parse(resp, nullptr, false);
m_user_info.connect_code = r.value("connectCode", m_user_info.connect_code);
m_user_info.latest_version = r.value("latestVersion", m_user_info.latest_version);
m_user_info.display_name = r.value("displayName", m_user_info.display_name);
if (r["chatMessages"].is_array())
{
m_user_info.chat_messages = r.value("chatMessages", SlippiUser::default_chat_messages);
if (m_user_info.chat_messages.size() != 16)
{
m_user_info.chat_messages = SlippiUser::default_chat_messages;
}
}
return slprs_user_get_is_logged_in(slprs_exi_device_ptr);
}

View file

@ -8,9 +8,18 @@
#include <vector>
#include "Common/CommonTypes.h"
// This class is currently a shim for the Rust user interface. We're doing it this way
// to begin migrating things over without needing to do larger invasive changes.
//
// The remaining methods in here are simply layers that direct the call over to the Rust side.
// A quirk of this is that we're using the EXI device pointer, so this class absolutely
// cannot outlive the EXI device - but we control that and just need to do our due diligence
// when making changes.
class SlippiUser
{
public:
// This type is filled in with data from the Rust side.
// Eventually, this entire class will disappear.
struct UserInfo
{
std::string uid = "";
@ -25,35 +34,22 @@ public:
std::vector<std::string> chat_messages;
};
SlippiUser();
SlippiUser(uintptr_t rs_exi_device_ptr);
~SlippiUser();
bool AttemptLogin();
void OpenLogInPage();
void UpdateApp();
bool UpdateApp();
void ListenForLogIn();
void LogOut();
void OverwriteLatestVersion(std::string version);
UserInfo GetUserInfo();
std::vector<std::string> GetUserChatMessages();
std::vector<std::string> GetDefaultChatMessages();
bool IsLoggedIn();
void FileListenThread();
const static std::vector<std::string> default_chat_messages;
protected:
std::string getUserFilePath();
UserInfo parseFile(std::string file_contents);
void deleteFile();
void overwriteFromServer();
UserInfo m_user_info;
bool m_is_logged_in = false;
const std::string URL_START = "https://users-rest-dot-slippi.uc.r.appspot.com/user";
CURL* m_curl = nullptr;
struct curl_slist* m_curl_header_list = nullptr;
std::vector<char> m_receive_buf;
std::thread m_file_listen_thread;
std::atomic<bool> m_run_thread;
// A pointer to a "shadow" EXI Device that lives on the Rust side of things.
// Do *not* do any cleanup of this! The EXI device will handle it.
uintptr_t slprs_exi_device_ptr;
};

View file

@ -565,7 +565,7 @@ endif()
#endif()
corrosion_import_crate(MANIFEST_PATH ${CMAKE_SOURCE_DIR}/Externals/SlippiRustExtensions/Cargo.toml ${RUST_FEATURES})
target_link_libraries(dolphin-emu PUBLIC slippi_rust_extensions)
target_link_libraries(dolphin-emu PUBLIC slippi-rust-extensions)
if(APPLE)
include(BundleUtilities)

View file

@ -410,9 +410,14 @@ void SetUserDirectory(std::string custom_path)
std::string home_path = std::string(home) + DIR_SEP;
#if defined(__APPLE__)
// Mainline Dolphin switched to storing things elsewhere some time ago.
// To get it working for now, let's just use the Slippi route.
user_path = File::GetBundleDirectory() + "/Contents/Resources/User" DIR_SEP;
// Since the Replays build shares the same identifier as the netplay build,
// we'll just have a netplay and playback folder inside the identifer similar to how
// the Launcher does it to keep with some convention.
#ifdef IS_PLAYBACK
user_path = File::GetApplicationSupportDirectory() + "/playback/User" DIR_SEP;
#else
user_path = File::GetApplicationSupportDirectory() + "/netplay/User" DIR_SEP;
#endif
#elif defined(ANDROID)
if (env_path)
{
@ -513,7 +518,7 @@ bool TriggerSTMPowerEvent()
#ifdef HAVE_X11
void InhibitScreenSaver(Window win, bool inhibit)
#else
void InhibitScreenSaver(bool inhibit)
void InhibitScreenSaver(bool inhibit)
#endif
{
// Inhibit the screensaver. Depending on the operating system this may also