/* 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 #include #include "paths.h" #include "retroarch.h" #include "verbosity.h" #include "msg_hash.h" #include "disk_control_interface.h" #ifdef HAVE_CHEEVOS #include "cheevos/cheevos.h" #endif /*****************/ /* Configuration */ /*****************/ /** * disk_control_reset_callback: * * 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)); } /** * disk_control_set_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; } /** * disk_control_set_ext_callback: * * 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 */ /**********/ /** * disk_control_enabled: * * Leaf function. * * @return 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 && 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; } /** * disk_control_append_enabled: * * Leaf function. * * @return 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 && disk_control->cb.replace_image_index && disk_control->cb.add_image_index) return true; return false; } /** * disk_control_image_label_enabled: * * Leaf function. * * @return true if core supports image labels * - get_image_label **/ bool disk_control_image_label_enabled( disk_control_interface_t *disk_control) { return disk_control && disk_control->cb.get_image_label; } /** * disk_control_initial_image_enabled: * * Leaf function. * * @return 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 && disk_control->cb.set_initial_image && disk_control->cb.get_image_path) return true; return false; } /***********/ /* Getters */ /***********/ /** * disk_control_get_eject_state: * * @return 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(); } /** * disk_control_get_num_images: * * @return 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(); } /** * disk_control_get_image_index: * * @return 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(); } /** * disk_control_get_image_label: * * 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 */ /***********/ /** * disk_control_get_index_set_msg: * * 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[128]; 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) { size_t _len = strlcpy(msg, success ? msg_hash_to_str(MSG_SETTING_DISK_IN_TRAY) : msg_hash_to_str(MSG_FAILED_TO_SET_DISK), len); if (has_label) snprintf( msg + _len, len - _len, ": %u/%u - %s", index + 1, num_images, image_label); else snprintf( msg + _len, len - _len, ": %u/%u", 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); } /** * disk_control_set_eject_state: * * 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)) strlcpy( msg, eject ? msg_hash_to_str(MSG_DISK_EJECTED) : msg_hash_to_str(MSG_DISK_CLOSED), sizeof(msg)); else { error = true; strlcpy( msg, eject ? msg_hash_to_str(MSG_VIRTUAL_DISK_TRAY_EJECT) : msg_hash_to_str(MSG_VIRTUAL_DISK_TRAY_CLOSE), sizeof(msg)); } if (!string_is_empty(msg)) { if (error) RARCH_ERR("[Disc]: %s\n", msg); else RARCH_LOG("[Disc]: %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); } #ifdef HAVE_CHEEVOS if (!error && !eject) { if (disk_control->cb.get_image_index && disk_control->cb.get_image_path) { char image_path[PATH_MAX_LENGTH] = ""; unsigned image_index = disk_control->cb.get_image_index(); if (disk_control->cb.get_image_path(image_index, image_path, sizeof(image_path))) rcheevos_change_disc(image_path, false); } } #endif return !error; } /** * disk_control_set_index: * * 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[NAME_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("[Disc]: %s\n", msg); else RARCH_LOG("[Disc]: %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) { char new_image_path[PATH_MAX_LENGTH] = {0}; /* Get current image index + path */ unsigned new_image_index = disk_control->cb.get_image_index(); bool 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; } /** * disk_control_set_index_next: * * 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("[Disc]: %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); } /** * disk_control_set_index_prev: * * 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("[Disc]: %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); } /** * disk_control_append_image: * * Appends specified image file to disk image list **/ bool disk_control_append_image( disk_control_interface_t *disk_control, const char *image_path) { size_t _len; bool initial_disk_ejected = false; unsigned initial_index = 0; unsigned new_index = 0; const char *image_filename = NULL; struct retro_game_info info = {0}; char msg[128]; /* 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; /* Get initial disk eject state */ initial_disk_ejected = disk_control_get_eject_state(disk_control); /* Cache initial image index */ initial_index = disk_control->cb.get_image_index(); /* If tray is currently closed, eject disk */ if (!initial_disk_ejected && !disk_control_set_eject_state(disk_control, true, false)) goto error; /* Append image */ if (!disk_control->cb.add_image_index()) goto error; if ((new_index = disk_control->cb.get_num_images()) < 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; /* If tray was initially closed, insert disk * (i.e. leave system in the state we found it) */ if ( !initial_disk_ejected && !disk_control_set_eject_state(disk_control, false, false)) goto error; /* Display log */ _len = strlcpy(msg, msg_hash_to_str(MSG_APPENDED_DISK), sizeof(msg)); msg[ _len] = ':'; msg[++_len] = ' '; msg[++_len] = '\0'; strlcpy(msg + _len, image_filename, sizeof(msg) - _len); RARCH_LOG("[Disc]: %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); if (!initial_disk_ejected) disk_control_set_eject_state(disk_control, false, false); _len = strlcpy(msg, msg_hash_to_str(MSG_FAILED_TO_APPEND_DISK), sizeof(msg)); msg[ _len] = ':'; msg[++_len] = ' '; msg[++_len] = '\0'; strlcpy(msg + _len, image_filename, sizeof(msg) - _len); runloop_msg_queue_push( msg, 0, 180, true, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); return false; } /*****************************/ /* 'Initial index' functions */ /*****************************/ /** * disk_control_set_initial_index: * * 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( "[Disc]: 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; } /** * disk_control_verify_initial_index: * * 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 verbosity, bool enabled) { 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 && 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 && enabled) { RARCH_ERR( "[Disc]: Failed to set initial disc 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); /* Ignore 'verbosity' setting - errors should * always be displayed */ 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("[Disc]: %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 */ if (verbosity) runloop_msg_queue_push( msg, 0, msg_duration, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); #ifdef HAVE_CHEEVOS if (image_index > 0) rcheevos_change_disc(disk_control->index_record.image_path, true); #endif } return success; } /** * disk_control_save_image_index: * * 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); }