mirror of
https://github.com/libretro/RetroArch.git
synced 2024-11-23 07:59:42 +00:00
553 lines
17 KiB
C
553 lines
17 KiB
C
#include <ctype.h>
|
|
#include <mist.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#include <retro_timers.h>
|
|
#include <compat/strl.h>
|
|
#include <string/stdstring.h>
|
|
#include <retro_miscellaneous.h>
|
|
|
|
#include "../input/input_driver.h"
|
|
#include "../menu/menu_driver.h"
|
|
#include "../menu/menu_entries.h"
|
|
#include "../retroarch.h"
|
|
#include "../runloop.h"
|
|
#include "paths.h"
|
|
#include "verbosity.h"
|
|
|
|
#include "steam.h"
|
|
|
|
static bool mist_initialized = false;
|
|
static bool mist_showing_osk = false;
|
|
static steam_core_dlc_list_t *mist_dlc_list = NULL;
|
|
static enum presence last_presence = PRESENCE_NONE;
|
|
|
|
void steam_init(void)
|
|
{
|
|
MistResult result = mist_subprocess_init();
|
|
|
|
if (MIST_IS_SUCCESS(result))
|
|
mist_initialized = true;
|
|
else
|
|
RARCH_ERR("[Steam]: Failed to initialize mist subprocess (%d-%d)\n", MIST_UNPACK_RESULT(result));
|
|
}
|
|
|
|
void steam_poll(void)
|
|
{
|
|
MistCallbackMsg callback;
|
|
steam_core_dlc_list_t *core_dlc_list;
|
|
bool has_callback = false;
|
|
settings_t* settings = config_get_ptr();
|
|
static bool has_poll_errored = false;
|
|
static bool has_rich_presence_enabled = false;
|
|
MistResult result = mist_poll();
|
|
if (MIST_IS_ERROR(result))
|
|
{
|
|
if (has_poll_errored)
|
|
return;
|
|
|
|
RARCH_ERR("[Steam]: Error polling (%d-%d)\n", MIST_UNPACK_RESULT(result));
|
|
|
|
has_poll_errored = true;
|
|
}
|
|
|
|
result = mist_next_callback(&has_callback, &callback);
|
|
if (MIST_IS_ERROR(result))
|
|
return;
|
|
|
|
while (has_callback && MIST_IS_SUCCESS(result))
|
|
{
|
|
switch (callback.callback)
|
|
{
|
|
/* Reload core info and Steam Core DLC mappings */
|
|
case MistCallback_DlcInstalled:
|
|
command_event(CMD_EVENT_CORE_INFO_INIT, NULL);
|
|
steam_get_core_dlcs(&core_dlc_list, false);
|
|
break;
|
|
/* The Steam OSK is dismissed */
|
|
case MistCallback_FloatingGamepadTextInputDismissed:
|
|
/* If we do not poll for input the callback might race condition and
|
|
will dismiss the input even when enter is pressed */
|
|
retro_sleep(50);
|
|
runloop_iterate();
|
|
menu_input_dialog_end();
|
|
break;
|
|
}
|
|
|
|
result = mist_next_callback(&has_callback, &callback);
|
|
}
|
|
|
|
/* Ensure rich presence state is correct */
|
|
if (settings->bools.steam_rich_presence_enable != has_rich_presence_enabled)
|
|
{
|
|
steam_update_presence(last_presence, true);
|
|
has_rich_presence_enabled = settings->bools.steam_rich_presence_enable;
|
|
}
|
|
}
|
|
|
|
steam_core_dlc_list_t *steam_core_dlc_list_new(size_t count)
|
|
{
|
|
steam_core_dlc_list_t *core_dlc_list = (steam_core_dlc_list_t*)
|
|
malloc(sizeof(*core_dlc_list));
|
|
|
|
core_dlc_list->list = (steam_core_dlc_t*)
|
|
malloc(count * sizeof(*core_dlc_list->list));
|
|
core_dlc_list->count = 0; /* This is incremented inside the setup function */
|
|
|
|
return core_dlc_list;
|
|
}
|
|
|
|
void steam_core_dlc_list_free(steam_core_dlc_list_t *list)
|
|
{
|
|
size_t i;
|
|
if (!list)
|
|
return;
|
|
|
|
for (i = 0; list->count > i; i++)
|
|
{
|
|
if (list->list[i].name)
|
|
free(list->list[i].name);
|
|
if (list->list[i].name_lower)
|
|
free(list->list[i].name_lower);
|
|
}
|
|
|
|
free(list->list);
|
|
free(list);
|
|
}
|
|
|
|
steam_core_dlc_t *steam_core_dlc_list_get(steam_core_dlc_list_t *list, size_t i)
|
|
{
|
|
if (!list || (i >= list->count))
|
|
return NULL;
|
|
|
|
return &list->list[i];
|
|
}
|
|
|
|
/* Sort the dlc cores alphabetically based on their name */
|
|
static int dlc_core_qsort_cmp(const void *a_, const void *b_)
|
|
{
|
|
const steam_core_dlc_t *a = (const steam_core_dlc_t*)a_;
|
|
const steam_core_dlc_t *b = (const steam_core_dlc_t*)b_;
|
|
|
|
return strcasecmp(a->name, b->name);
|
|
}
|
|
|
|
/* Find core info for dlcs
|
|
* TODO: This currently only uses core info for cores that are installed */
|
|
core_info_t* steam_find_core_info_for_dlc(const char* name)
|
|
{
|
|
int i;
|
|
core_info_list_t *core_info_list = NULL;
|
|
core_info_get_list(&core_info_list);
|
|
|
|
if (!core_info_list)
|
|
return NULL;
|
|
|
|
for (i = 0; core_info_list->count > i; i++)
|
|
{
|
|
char core_info_name[NAME_MAX_LENGTH];
|
|
core_info_t *core_info = core_info_get(core_info_list, i);
|
|
/* Find the opening parenthesis for the core name */
|
|
char *start = strchr(core_info->display_name, '(');
|
|
if (!start)
|
|
continue;
|
|
|
|
/* Skip the first parenthesis and copy it to the stack */
|
|
strlcpy(core_info_name, start + 1, sizeof(core_info_name));
|
|
|
|
/* Null terminate at the closing parenthesis. */
|
|
char *end = strchr((const char*)&core_info_name, ')');
|
|
if (!end)
|
|
continue;
|
|
|
|
*end = '\0';
|
|
|
|
/* Make it lowercase */
|
|
string_to_lower((char*)&core_info_name);
|
|
|
|
/* Check if it matches */
|
|
if (strcasecmp(core_info_name, name) == 0)
|
|
return core_info;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/* Generate a list with core dlcs
|
|
* Needs to be called after initialization because it uses core info */
|
|
MistResult steam_generate_core_dlcs_list(steam_core_dlc_list_t **list)
|
|
{
|
|
int count, i;
|
|
steam_core_dlc_list_t *dlc_list = NULL;
|
|
char dlc_name[PATH_MAX_LENGTH] = { 0 };
|
|
bool available = false;
|
|
MistResult result = mist_steam_apps_get_dlc_count(&count);
|
|
if (MIST_IS_ERROR(result))
|
|
goto error;
|
|
|
|
dlc_list = steam_core_dlc_list_new(count);
|
|
for (i = 0; count > i; i++)
|
|
{
|
|
steam_core_dlc_t core_dlc;
|
|
|
|
result = mist_steam_apps_get_dlc_data_by_index(i, &core_dlc.app_id, &available, (char*)&dlc_name, PATH_MAX_LENGTH);
|
|
if (MIST_IS_ERROR(result))
|
|
goto error;
|
|
|
|
/* Strip away the "RetroArch - " prefix if present */
|
|
if (strncmp(dlc_name, "RetroArch - ", sizeof("RetroArch - ") - 1) == 0)
|
|
core_dlc.name = strdup(dlc_name + sizeof("RetroArch - ") - 1);
|
|
else
|
|
core_dlc.name = strdup(dlc_name);
|
|
|
|
/* Make a lower case version */
|
|
core_dlc.name_lower = strdup(core_dlc.name);
|
|
string_to_lower(core_dlc.name_lower);
|
|
|
|
core_dlc.core_info = steam_find_core_info_for_dlc(core_dlc.name_lower);
|
|
|
|
dlc_list->list[i] = core_dlc;
|
|
dlc_list->count++;
|
|
}
|
|
|
|
/* Sort the list */
|
|
qsort(dlc_list->list, dlc_list->count,
|
|
sizeof(steam_core_dlc_t), dlc_core_qsort_cmp);
|
|
|
|
*list = dlc_list;
|
|
return MistResult_Success;
|
|
|
|
error:
|
|
if (dlc_list)
|
|
steam_core_dlc_list_free(dlc_list);
|
|
|
|
return result;
|
|
}
|
|
|
|
MistResult steam_get_core_dlcs(steam_core_dlc_list_t **list, bool cached)
|
|
{
|
|
MistResult result;
|
|
steam_core_dlc_list_t *new_list = NULL;
|
|
|
|
if (cached && mist_dlc_list)
|
|
{
|
|
*list = mist_dlc_list;
|
|
return MistResult_Success;
|
|
}
|
|
|
|
result = steam_generate_core_dlcs_list(&new_list);
|
|
if (MIST_IS_ERROR(result))
|
|
return result;
|
|
|
|
if (mist_dlc_list)
|
|
steam_core_dlc_list_free(mist_dlc_list);
|
|
|
|
mist_dlc_list = new_list;
|
|
*list = new_list;
|
|
|
|
return MistResult_Success;
|
|
}
|
|
|
|
steam_core_dlc_t* steam_get_core_dlc_by_name(
|
|
steam_core_dlc_list_t *list, const char *name)
|
|
{
|
|
int i;
|
|
steam_core_dlc_t *core_info;
|
|
|
|
for (i = 0; list->count > i; i++)
|
|
{
|
|
core_info = steam_core_dlc_list_get(list, i);
|
|
if (strcasecmp(core_info->name, name) == 0)
|
|
return core_info;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
void steam_install_core_dlc(steam_core_dlc_t *core_dlc)
|
|
{
|
|
char msg[128] = { 0 };
|
|
bool downloading = false;
|
|
bool installed = false;
|
|
uint64_t bytes_downloaded = 0;
|
|
uint64_t bytes_total = 0;
|
|
/* Check if the core is already being downloaded */
|
|
MistResult result = mist_steam_apps_get_dlc_download_progress(core_dlc->app_id, &downloading, &bytes_downloaded, &bytes_total);
|
|
if (MIST_IS_ERROR(result))
|
|
goto error;
|
|
|
|
/* Check if the core is already installed */
|
|
result = mist_steam_apps_is_dlc_installed(core_dlc->app_id, &installed);
|
|
if (MIST_IS_ERROR(result))
|
|
goto error;
|
|
|
|
if (downloading || installed)
|
|
{
|
|
runloop_msg_queue_push(msg_hash_to_str(MSG_CORE_STEAM_CURRENTLY_DOWNLOADING), 1, 180, true, NULL,
|
|
MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_ERROR);
|
|
return;
|
|
}
|
|
|
|
result = mist_steam_apps_install_dlc(core_dlc->app_id);
|
|
if (MIST_IS_ERROR(result))
|
|
goto error;
|
|
|
|
task_push_steam_core_dlc_install(core_dlc->app_id, core_dlc->name);
|
|
|
|
return;
|
|
error:
|
|
snprintf(msg, sizeof(msg), "%s: (%d-%d)",
|
|
msg_hash_to_str(MSG_ERROR),
|
|
MIST_UNPACK_RESULT(result));
|
|
|
|
runloop_msg_queue_push(msg, 1, 180, true, NULL,
|
|
MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_ERROR);
|
|
|
|
RARCH_ERR("[Steam]: Error installing DLC %d (%d-%d)\n", core_dlc->app_id, MIST_UNPACK_RESULT(result));
|
|
}
|
|
|
|
void steam_uninstall_core_dlc(steam_core_dlc_t *core_dlc)
|
|
{
|
|
char msg[128] = { 0 };
|
|
|
|
MistResult result = mist_steam_apps_uninstall_dlc(core_dlc->app_id);
|
|
|
|
if (MIST_IS_ERROR(result))
|
|
goto error;
|
|
|
|
runloop_msg_queue_push(msg_hash_to_str(MSG_CORE_STEAM_UNINSTALLED), 1, 180, true, NULL,
|
|
MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
|
|
return;
|
|
|
|
error:
|
|
snprintf(msg, sizeof(msg), "%s: (%d-%d)",
|
|
msg_hash_to_str(MSG_ERROR),
|
|
MIST_UNPACK_RESULT(result));
|
|
|
|
runloop_msg_queue_push(msg, 1, 180, true, NULL,
|
|
MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_ERROR);
|
|
|
|
RARCH_ERR("[Steam]: Error uninstalling DLC %d (%d-%d)\n", core_dlc->app_id, MIST_UNPACK_RESULT(result));
|
|
}
|
|
|
|
bool steam_open_osk(void)
|
|
{
|
|
bool shown = false;
|
|
bool on_deck = false;
|
|
video_driver_state_t *video_st = video_state_get_ptr();
|
|
|
|
/* Only open the Steam OSK if running on a Steam Deck,
|
|
as currently the Big Picture OSK seems to be semi-broken */
|
|
mist_steam_utils_is_steam_running_on_steam_deck(&on_deck);
|
|
if (!on_deck)
|
|
return false;
|
|
|
|
mist_steam_utils_show_floating_gamepad_text_input(
|
|
MistFloatingGamepadTextInputMode_SingleLine,
|
|
0,
|
|
0,
|
|
video_st->width,
|
|
video_st->height / 2,
|
|
&shown
|
|
);
|
|
|
|
mist_showing_osk = shown;
|
|
|
|
return shown;
|
|
}
|
|
|
|
bool steam_has_osk_open(void)
|
|
{
|
|
return mist_showing_osk;
|
|
}
|
|
|
|
void steam_update_presence(enum presence presence, bool force)
|
|
{
|
|
settings_t* settings = config_get_ptr();
|
|
|
|
if (!mist_initialized)
|
|
return;
|
|
|
|
/* Avoid spamming steam with presence updates */
|
|
if (presence == last_presence && !force)
|
|
return;
|
|
last_presence = presence;
|
|
|
|
/* Ensure rich presence is enabled */
|
|
if (!settings->bools.steam_rich_presence_enable)
|
|
{
|
|
mist_steam_friends_clear_rich_presence();
|
|
return;
|
|
}
|
|
|
|
switch (presence)
|
|
{
|
|
case PRESENCE_MENU:
|
|
mist_steam_friends_set_rich_presence("steam_display", "#Status_InMenu");
|
|
break;
|
|
case PRESENCE_GAME_PAUSED:
|
|
mist_steam_friends_set_rich_presence("steam_display", "#Status_Paused");
|
|
break;
|
|
case PRESENCE_GAME:
|
|
{
|
|
size_t _len;
|
|
char content[PATH_MAX_LENGTH];
|
|
const char *label = NULL;
|
|
const struct playlist_entry *entry = NULL;
|
|
core_info_t *core_info = NULL;
|
|
playlist_t *current_playlist = playlist_get_cached();
|
|
|
|
core_info_get_current_core(&core_info);
|
|
|
|
if (current_playlist)
|
|
{
|
|
playlist_get_index_by_path(
|
|
current_playlist,
|
|
path_get(RARCH_PATH_CONTENT),
|
|
&entry);
|
|
|
|
if (entry && !string_is_empty(entry->label))
|
|
label = entry->label;
|
|
}
|
|
|
|
if (!label)
|
|
label = path_basename(path_get(RARCH_PATH_BASENAME));
|
|
|
|
switch(settings->uints.steam_rich_presence_format)
|
|
{
|
|
case STEAM_RICH_PRESENCE_FORMAT_CONTENT:
|
|
strlcpy(content, label, sizeof(content));
|
|
break;
|
|
case STEAM_RICH_PRESENCE_FORMAT_CORE:
|
|
if (core_info)
|
|
strlcpy(content, core_info->core_name, sizeof(content));
|
|
else
|
|
{
|
|
content[0] = 'N';
|
|
content[1] = '/';
|
|
content[2] = 'A';
|
|
content[3] = '\0';
|
|
}
|
|
break;
|
|
case STEAM_RICH_PRESENCE_FORMAT_SYSTEM:
|
|
if (core_info)
|
|
strlcpy(content, core_info->systemname, sizeof(content));
|
|
else
|
|
{
|
|
content[0] = 'N';
|
|
content[1] = '/';
|
|
content[2] = 'A';
|
|
content[3] = '\0';
|
|
}
|
|
break;
|
|
case STEAM_RICH_PRESENCE_FORMAT_CONTENT_SYSTEM:
|
|
_len = strlcpy(content, label, sizeof(content));
|
|
content[_len ] = ' ';
|
|
content[_len+1] = '(';
|
|
content[_len+2] = '\0';
|
|
if (core_info)
|
|
{
|
|
_len += 2;
|
|
_len += strlcpy(content + _len, core_info->systemname,
|
|
sizeof(content) - _len);
|
|
content[_len ] = ')';
|
|
content[_len+1] = '\0';
|
|
}
|
|
else
|
|
{
|
|
content[_len+2] = 'N';
|
|
content[_len+3] = '/';
|
|
content[_len+4] = 'A';
|
|
content[_len+5] = ')';
|
|
content[_len+6] = '\0';
|
|
}
|
|
break;
|
|
case STEAM_RICH_PRESENCE_FORMAT_CONTENT_CORE:
|
|
_len = strlcpy(content, label, sizeof(content));
|
|
content[_len ] = ' ';
|
|
content[_len+1] = '(';
|
|
content[_len+2] = '\0';
|
|
if (core_info)
|
|
{
|
|
_len += 2;
|
|
_len += strlcpy(content + _len, core_info->core_name,
|
|
sizeof(content) - _len);
|
|
content[_len ] = ')';
|
|
content[_len+1] = '\0';
|
|
}
|
|
else
|
|
{
|
|
content[_len+2] = 'N';
|
|
content[_len+3] = '/';
|
|
content[_len+4] = 'A';
|
|
content[_len+5] = ')';
|
|
content[_len+6] = '\0';
|
|
}
|
|
break;
|
|
case STEAM_RICH_PRESENCE_FORMAT_CONTENT_SYSTEM_CORE:
|
|
_len = strlcpy(content, label, sizeof(content));
|
|
content[_len ] = ' ';
|
|
content[_len+1] = '(';
|
|
content[_len+2] = '\0';
|
|
if (core_info)
|
|
{
|
|
_len += 2;
|
|
_len += strlcpy(content + _len, core_info->systemname,
|
|
sizeof(content) - _len);
|
|
content[_len ] = ' ';
|
|
content[_len+1] = '-';
|
|
content[_len+2] = ' ';
|
|
_len += 3;
|
|
_len += strlcpy(content + _len, core_info->core_name,
|
|
sizeof(content) - _len);
|
|
content[_len ] = ')';
|
|
content[_len+1] = '\0';
|
|
}
|
|
else
|
|
{
|
|
content[_len+2] = 'N';
|
|
content[_len+3] = '/';
|
|
content[_len+4] = 'A';
|
|
content[_len+5] = ' ';
|
|
content[_len+6] = '-';
|
|
content[_len+7] = ' ';
|
|
content[_len+8] = 'N';
|
|
content[_len+9] = '/';
|
|
content[_len+10] = 'A';
|
|
content[_len+11] = ')';
|
|
content[_len+12] = '\0';
|
|
}
|
|
break;
|
|
case STEAM_RICH_PRESENCE_FORMAT_NONE:
|
|
default:
|
|
content[0] = '\0';
|
|
break;
|
|
}
|
|
|
|
|
|
mist_steam_friends_set_rich_presence("content", content);
|
|
mist_steam_friends_set_rich_presence("steam_display",
|
|
settings->uints.steam_rich_presence_format != STEAM_RICH_PRESENCE_FORMAT_NONE
|
|
? "#Status_RunningContent" : "#Status_Running" );
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void steam_deinit(void)
|
|
{
|
|
MistResult result = mist_subprocess_deinit();
|
|
|
|
/* Free the cached dlc list */
|
|
if (mist_dlc_list)
|
|
steam_core_dlc_list_free(mist_dlc_list);
|
|
|
|
if (MIST_IS_SUCCESS(result))
|
|
mist_initialized = false;
|
|
else
|
|
RARCH_ERR("[Steam]: Failed to deinitialize mist subprocess (%d-%d)\n", MIST_UNPACK_RESULT(result));
|
|
}
|