RetroArch/disk_control_interface.c
2020-03-05 18:29:48 +01:00

796 lines
23 KiB
C

/* Copyright (C) 2010-2020 The RetroArch team
*
* ---------------------------------------------------------------------------------------
* The following license statement only applies to this file (disk_control_interface.c).
* ---------------------------------------------------------------------------------------
*
* Permission is hereby granted, free of charge,
* to any person obtaining a copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include <string/stdstring.h>
#include <file/file_path.h>
#include "paths.h"
#include "retroarch.h"
#include "verbosity.h"
#include "msg_hash.h"
#include "disk_control_interface.h"
/*****************/
/* Configuration */
/*****************/
/* Sets all disk interface callback functions
* to NULL */
static void disk_control_reset_callback(
disk_control_interface_t *disk_control)
{
if (!disk_control)
return;
memset(&disk_control->cb, 0, sizeof(struct retro_disk_control_ext_callback));
}
/* Set v0 disk interface callback functions */
void disk_control_set_callback(
disk_control_interface_t *disk_control,
const struct retro_disk_control_callback *cb)
{
if (!disk_control)
return;
disk_control_reset_callback(disk_control);
if (!cb)
return;
disk_control->cb.set_eject_state = cb->set_eject_state;
disk_control->cb.get_eject_state = cb->get_eject_state;
disk_control->cb.get_image_index = cb->get_image_index;
disk_control->cb.set_image_index = cb->set_image_index;
disk_control->cb.get_num_images = cb->get_num_images;
disk_control->cb.replace_image_index = cb->replace_image_index;
disk_control->cb.add_image_index = cb->add_image_index;
}
/* Set v1+ disk interface callback functions */
void disk_control_set_ext_callback(
disk_control_interface_t *disk_control,
const struct retro_disk_control_ext_callback *cb)
{
if (!disk_control)
return;
disk_control_reset_callback(disk_control);
if (!cb)
return;
disk_control->cb.set_eject_state = cb->set_eject_state;
disk_control->cb.get_eject_state = cb->get_eject_state;
disk_control->cb.get_image_index = cb->get_image_index;
disk_control->cb.set_image_index = cb->set_image_index;
disk_control->cb.get_num_images = cb->get_num_images;
disk_control->cb.replace_image_index = cb->replace_image_index;
disk_control->cb.add_image_index = cb->add_image_index;
disk_control->cb.set_initial_image = cb->set_initial_image;
disk_control->cb.get_image_path = cb->get_image_path;
disk_control->cb.get_image_label = cb->get_image_label;
}
/**********/
/* Status */
/**********/
/* Returns true if core supports basic disk
* control functionality
* - set_eject_state
* - get_eject_state
* - get_image_index
* - set_image_index
* - get_num_images */
bool disk_control_enabled(
disk_control_interface_t *disk_control)
{
if (!disk_control)
return false;
if (disk_control->cb.set_eject_state &&
disk_control->cb.get_eject_state &&
disk_control->cb.get_image_index &&
disk_control->cb.set_image_index &&
disk_control->cb.get_num_images)
return true;
return false;
}
/* Returns true if core supports disk append
* functionality
* - replace_image_index
* - add_image_index */
bool disk_control_append_enabled(
disk_control_interface_t *disk_control)
{
if (!disk_control)
return false;
if (disk_control->cb.replace_image_index &&
disk_control->cb.add_image_index)
return true;
return false;
}
/* Returns true if core supports image
* labels
* - get_image_label */
bool disk_control_image_label_enabled(
disk_control_interface_t *disk_control)
{
if (!disk_control || !disk_control->cb.get_image_label)
return false;
return true;
}
/* Returns true if core supports setting
* initial disk index
* - set_initial_image
* - get_image_path */
bool disk_control_initial_image_enabled(
disk_control_interface_t *disk_control)
{
if (!disk_control)
return false;
if (disk_control->cb.set_initial_image &&
disk_control->cb.get_image_path)
return true;
return false;
}
/***********/
/* Getters */
/***********/
/* Returns true if disk is currently ejected */
bool disk_control_get_eject_state(
disk_control_interface_t *disk_control)
{
if (!disk_control || !disk_control->cb.get_eject_state)
return false;
return disk_control->cb.get_eject_state();
}
/* Returns number of disk images registered
* by the core */
unsigned disk_control_get_num_images(
disk_control_interface_t *disk_control)
{
if (!disk_control || !disk_control->cb.get_num_images)
return 0;
return disk_control->cb.get_num_images();
}
/* Returns currently selected disk image index */
unsigned disk_control_get_image_index(
disk_control_interface_t *disk_control)
{
if (!disk_control || !disk_control->cb.get_image_index)
return 0;
return disk_control->cb.get_image_index();
}
/* Fetches core-provided disk image label
* (label is set to an empty string if core
* does not support image labels) */
void disk_control_get_image_label(
disk_control_interface_t *disk_control,
unsigned index, char *label, size_t len)
{
if (!label || len < 1)
return;
if (!disk_control)
goto error;
if (!disk_control->cb.get_image_label)
goto error;
if (!disk_control->cb.get_image_label(index, label, len))
goto error;
return;
error:
label[0] = '\0';
}
/***********/
/* Setters */
/***********/
/* Generates an appropriate log/notification message
* for a disk index change event */
static void disk_control_get_index_set_msg(
disk_control_interface_t *disk_control,
unsigned num_images, unsigned index, bool success,
unsigned *msg_duration, char *msg, size_t len)
{
bool has_label = false;
char image_label[PATH_MAX_LENGTH];
image_label[0] = '\0';
if (!disk_control || !msg_duration || !msg || len < 1)
return;
/* Attempt to get image label */
if (index < num_images)
{
disk_control_get_image_label(
disk_control, index, image_label, sizeof(image_label));
has_label = !string_is_empty(image_label);
}
/* Get message duration
* > Default is 60
* > If a label is shown, then increase duration by 50%
* > For errors, duration is always 180 */
*msg_duration = success ?
(has_label ? 90 : 60) :
180;
/* Check whether image was inserted or removed */
if (index < num_images)
{
if (has_label)
snprintf(
msg, len, "%s: %u/%u - %s",
success ? msg_hash_to_str(MSG_SETTING_DISK_IN_TRAY) :
msg_hash_to_str(MSG_FAILED_TO_SET_DISK),
index + 1, num_images, image_label);
else
snprintf(
msg, len, "%s: %u/%u",
success ? msg_hash_to_str(MSG_SETTING_DISK_IN_TRAY) :
msg_hash_to_str(MSG_FAILED_TO_SET_DISK),
index + 1, num_images);
}
else
strlcpy(
msg,
success ? msg_hash_to_str(MSG_REMOVED_DISK_FROM_TRAY) :
msg_hash_to_str(MSG_FAILED_TO_REMOVE_DISK_FROM_TRAY),
len);
}
/* Sets the eject state of the virtual disk tray */
bool disk_control_set_eject_state(
disk_control_interface_t *disk_control,
bool eject, bool verbosity)
{
bool error = false;
char msg[128];
msg[0] = '\0';
if (!disk_control || !disk_control->cb.set_eject_state)
return false;
/* Set eject state */
if (disk_control->cb.set_eject_state(eject))
snprintf(
msg, sizeof(msg), "%s %s",
eject ? msg_hash_to_str(MSG_DISK_EJECTED) :
msg_hash_to_str(MSG_DISK_CLOSED),
msg_hash_to_str(MSG_VIRTUAL_DISK_TRAY));
else
{
error = true;
snprintf(
msg, sizeof(msg), "%s %s %s",
msg_hash_to_str(MSG_FAILED_TO),
eject ? msg_hash_to_str(MSG_VIRTUAL_DISK_TRAY_EJECT) :
msg_hash_to_str(MSG_VIRTUAL_DISK_TRAY_CLOSE),
msg_hash_to_str(MSG_VIRTUAL_DISK_TRAY));
}
if (!string_is_empty(msg))
{
if (error)
RARCH_ERR("%s\n", msg);
else
RARCH_LOG("%s\n", msg);
/* Errors should always be displayed */
if (verbosity || error)
runloop_msg_queue_push(
msg, 1, error ? 180 : 60,
true, NULL,
MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
}
return !error;
}
/* Sets currently selected disk index
* NOTE: Will fail if disk is not currently ejected */
bool disk_control_set_index(
disk_control_interface_t *disk_control,
unsigned index, bool verbosity)
{
bool error = false;
unsigned num_images = 0;
unsigned msg_duration = 0;
char msg[PATH_MAX_LENGTH];
msg[0] = '\0';
if (!disk_control)
return false;
if (!disk_control->cb.get_eject_state ||
!disk_control->cb.get_num_images ||
!disk_control->cb.set_image_index)
return false;
/* Ensure that disk is currently ejected */
if (!disk_control->cb.get_eject_state())
return false;
/* Get current number of disk images */
num_images = disk_control->cb.get_num_images();
/* Perform 'set index' action */
error = !disk_control->cb.set_image_index(index);
/* Get log/notification message */
disk_control_get_index_set_msg(
disk_control, num_images, index, !error,
&msg_duration, msg, sizeof(msg));
/* Output log/notification message */
if (!string_is_empty(msg))
{
if (error)
RARCH_ERR("%s\n", msg);
else
RARCH_LOG("%s\n", msg);
/* Errors should always be displayed */
if (verbosity || error)
runloop_msg_queue_push(
msg, 1, msg_duration,
true, NULL,
MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
}
/* If operation was successful, update disk
* index record (if enabled) */
if (!error && disk_control->record_enabled)
{
if (disk_control->cb.get_image_index &&
disk_control->cb.get_image_path)
{
bool image_path_valid = false;
unsigned new_image_index = 0;
char new_image_path[PATH_MAX_LENGTH];
new_image_path[0] = '\0';
/* Get current image index + path */
new_image_index = disk_control->cb.get_image_index();
image_path_valid = disk_control->cb.get_image_path(
new_image_index, new_image_path, sizeof(new_image_path));
if (image_path_valid)
disk_index_file_set(
&disk_control->index_record,
new_image_index, new_image_path);
else
disk_index_file_set(
&disk_control->index_record, 0, NULL);
}
}
return !error;
}
/* Increments selected disk index */
bool disk_control_set_index_next(
disk_control_interface_t *disk_control,
bool verbosity)
{
unsigned num_images = 0;
unsigned image_index = 0;
bool disk_next_enable = false;
if (!disk_control)
return false;
if (!disk_control->cb.get_num_images ||
!disk_control->cb.get_image_index)
return false;
num_images = disk_control->cb.get_num_images();
image_index = disk_control->cb.get_image_index();
/* Would seem more sensible to check (num_images > 1)
* here, but seems we need to be able to cycle the
* same image for legacy reasons... */
disk_next_enable = (num_images > 0) && (num_images != UINT_MAX);
if (!disk_next_enable)
{
RARCH_ERR("%s.\n", msg_hash_to_str(MSG_GOT_INVALID_DISK_INDEX));
return false;
}
if (image_index < (num_images - 1))
image_index++;
return disk_control_set_index(disk_control, image_index, verbosity);
}
/* Decrements selected disk index */
bool disk_control_set_index_prev(
disk_control_interface_t *disk_control,
bool verbosity)
{
unsigned num_images = 0;
unsigned image_index = 0;
bool disk_prev_enable = false;
if (!disk_control)
return false;
if (!disk_control->cb.get_num_images ||
!disk_control->cb.get_image_index)
return false;
num_images = disk_control->cb.get_num_images();
image_index = disk_control->cb.get_image_index();
/* Would seem more sensible to check (num_images > 1)
* here, but seems we need to be able to cycle the
* same image for legacy reasons... */
disk_prev_enable = (num_images > 0);
if (!disk_prev_enable)
{
RARCH_ERR("%s.\n", msg_hash_to_str(MSG_GOT_INVALID_DISK_INDEX));
return false;
}
if (image_index > 0)
image_index--;
return disk_control_set_index(disk_control, image_index, verbosity);
}
/* Appends specified image file to disk image list */
bool disk_control_append_image(
disk_control_interface_t *disk_control,
const char *image_path)
{
unsigned initial_index = 0;
unsigned new_index = 0;
const char *image_filename = NULL;
struct retro_game_info info = {0};
char msg[128];
msg[0] = '\0';
/* Sanity check. If any of these fail then a
* frontend error has occurred - we will not
* deal with that here */
if (!disk_control)
return false;
if (!disk_control->cb.get_image_index ||
!disk_control->cb.get_num_images ||
!disk_control->cb.add_image_index ||
!disk_control->cb.replace_image_index ||
!disk_control->cb.get_eject_state)
return false;
if (string_is_empty(image_path))
return false;
image_filename = path_basename(image_path);
if (string_is_empty(image_filename))
return false;
/* Cache initial image index */
initial_index = disk_control->cb.get_image_index();
/* Eject disk */
if (!disk_control_set_eject_state(disk_control, true, false))
goto error;
/* Append image */
if (!disk_control->cb.add_image_index())
goto error;
new_index = disk_control->cb.get_num_images();
if (new_index < 1)
goto error;
new_index--;
info.path = image_path;
if (!disk_control->cb.replace_image_index(new_index, &info))
goto error;
/* Set new index */
if (!disk_control_set_index(disk_control, new_index, false))
goto error;
/* Insert disk */
if (!disk_control_set_eject_state(disk_control, false, false))
goto error;
/* Display log */
snprintf(
msg, sizeof(msg), "%s: %s",
msg_hash_to_str(MSG_APPENDED_DISK), image_filename);
RARCH_LOG("%s\n", msg);
/* This message should always be displayed, since
* the menu itself does not provide sufficient
* visual feedback */
runloop_msg_queue_push(
msg, 0, 120,
true, NULL,
MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
return true;
error:
/* If we reach this point then everything is
* broken and the disk control interface is
* in an undefined state. Try to restore some
* sanity by reinserting the original disk...
* NOTE: If this fails then it's game over -
* just display the error notification and
* hope for the best... */
if (!disk_control->cb.get_eject_state())
disk_control_set_eject_state(disk_control, true, false);
disk_control_set_index(disk_control, initial_index, false);
disk_control_set_eject_state(disk_control, false, false);
snprintf(
msg, sizeof(msg), "%s: %s",
msg_hash_to_str(MSG_FAILED_TO_APPEND_DISK), image_filename);
runloop_msg_queue_push(
msg, 0, 180,
true, NULL,
MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
return false;
}
/*****************************/
/* 'Initial index' functions */
/*****************************/
/* Attempts to set current core's initial disk index.
* > disk_control->record_enabled will be set to
* 'false' if core does not support initial
* index functionality
* > disk_control->index_record will be loaded
* from file (if an existing record is found)
* NOTE: Must be called immediately before
* loading content */
bool disk_control_set_initial_index(
disk_control_interface_t *disk_control,
const char *content_path,
const char *dir_savefile)
{
if (!disk_control)
return false;
if (string_is_empty(content_path))
goto error;
/* Check that 'initial index' functionality is enabled */
if (!disk_control->cb.set_initial_image ||
!disk_control->cb.get_num_images ||
!disk_control->cb.get_image_index ||
!disk_control->cb.get_image_path)
goto error;
/* Attempt to initialise disk index record (reading
* from disk, if file exists) */
disk_control->record_enabled = disk_index_file_init(
&disk_control->index_record,
content_path, dir_savefile);
/* If record is enabled and initial index is *not*
* zero, notify current core */
if (disk_control->record_enabled &&
(disk_control->index_record.image_index != 0))
{
if (!disk_control->cb.set_initial_image(
disk_control->index_record.image_index,
disk_control->index_record.image_path))
{
/* Note: We don't bother with an on-screen
* notification at this stage, since an error
* here may not matter (have to wait until
* disk index is verified) */
RARCH_ERR(
"Failed to set initial disk index: [%u] %s\n",
disk_control->index_record.image_index,
disk_control->index_record.image_path);
return false;
}
}
return true;
error:
disk_control->record_enabled = false;
return false;
}
/* Checks that initial index has been set correctly
* and provides user notification.
* > Sets disk_control->initial_num_images if
* if functionality is supported by core
* NOTE: Must be called immediately after
* loading content */
bool disk_control_verify_initial_index(disk_control_interface_t *disk_control)
{
bool success = false;
unsigned image_index = 0;
char image_path[PATH_MAX_LENGTH];
image_path[0] = '\0';
if (!disk_control)
return false;
/* If index record is disabled, can return immediately */
if (!disk_control->record_enabled)
return false;
/* Check that 'initial index' functionality is enabled */
if (!disk_control->cb.set_initial_image ||
!disk_control->cb.get_num_images ||
!disk_control->cb.get_image_index ||
!disk_control->cb.get_image_path)
return false;
/* Cache initial number of images
* (required for error checking when saving
* disk index file) */
disk_control->initial_num_images =
disk_control->cb.get_num_images();
/* Get current image index + path */
image_index = disk_control->cb.get_image_index();
if (disk_control->cb.get_image_path(
image_index, image_path, sizeof(image_path)))
{
/* Check whether index + path match set
* values
* > Note that if set index was zero and
* set path was empty, we ignore the path
* read here (since this corresponds to a
* 'first run', where no existing disk index
* file was present) */
if ((image_index == disk_control->index_record.image_index) &&
(string_is_equal(image_path, disk_control->index_record.image_path) ||
((disk_control->index_record.image_index == 0) &&
string_is_empty(disk_control->index_record.image_path))))
success = true;
}
/* If current disk is incorrect, notify user */
if (!success)
{
RARCH_ERR(
"Failed to set initial disk index:\n> Expected [%u] %s\n> Detected [%u] %s\n",
disk_control->index_record.image_index + 1,
disk_control->index_record.image_path,
image_index + 1,
image_path);
runloop_msg_queue_push(
msg_hash_to_str(MSG_FAILED_TO_SET_INITIAL_DISK),
0, 60,
true, NULL,
MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
/* Since a failure here typically means that the
* original M3U content file has been altered,
* any existing disk index record file will be
* invalid. We therefore 'reset' and save the disk
* index record to prevent a repeat of the error on
* the next run */
disk_index_file_set(&disk_control->index_record, 0, NULL);
disk_index_file_save(&disk_control->index_record);
}
/* If current disk is correct and recorded image
* path is empty (i.e. first run), need to register
* current image path */
else if (string_is_empty(disk_control->index_record.image_path))
disk_index_file_set(
&disk_control->index_record, image_index, image_path);
/* Regardless of success/failure, notify user of
* current disk index *if* more than one disk
* is available */
if (disk_control->initial_num_images > 1)
{
unsigned msg_duration = 0;
char msg[PATH_MAX_LENGTH];
msg[0] = '\0';
disk_control_get_index_set_msg(
disk_control, disk_control->initial_num_images, image_index, true,
&msg_duration, msg, sizeof(msg));
RARCH_LOG("%s\n", msg);
/* Note: Do not flush message queue here, since
* it is likely other notifications will be
* generated before setting the disk index, and
* we do not want to 'overwrite' them */
runloop_msg_queue_push(
msg,
0, msg_duration,
false, NULL,
MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
}
return success;
}
/* Saves current disk index to file, if supported
* by current core */
bool disk_control_save_image_index(disk_control_interface_t *disk_control)
{
if (!disk_control)
return false;
/* If index record is disabled, can return immediately */
if (!disk_control->record_enabled)
return false;
/* If core started with less than two disks,
* then a disk index record is unnecessary */
if (disk_control->initial_num_images < 2)
return false;
/* If current index is greater than initial
* number of disks then user has appended a
* disk and it is currently active. This setup
* *cannot* be restored, so cancel the file save */
if (disk_control->index_record.image_index >=
disk_control->initial_num_images)
return false;
/* Save record */
return disk_index_file_save(&disk_control->index_record);
}