| 1 | /* Copyright (C) 2010-2020 The RetroArch team |
| 2 | * |
| 3 | * --------------------------------------------------------------------------------------- |
| 4 | * The following license statement only applies to this file (media_detect_cd.c). |
| 5 | * --------------------------------------------------------------------------------------- |
| 6 | * |
| 7 | * Permission is hereby granted, free of charge, |
| 8 | * to any person obtaining a copy of this software and associated documentation files (the "Software"), |
| 9 | * to deal in the Software without restriction, including without limitation the rights to |
| 10 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, |
| 11 | * and to permit persons to whom the Software is furnished to do so, subject to the following conditions: |
| 12 | * |
| 13 | * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. |
| 14 | * |
| 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, |
| 16 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. |
| 18 | * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, |
| 19 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| 20 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
| 21 | */ |
| 22 | |
| 23 | #include <media/media_detect_cd.h> |
| 24 | #include <streams/file_stream.h> |
| 25 | #include <string/stdstring.h> |
| 26 | #include <file/file_path.h> |
| 27 | #include <retro_miscellaneous.h> |
| 28 | |
| 29 | /*#define MEDIA_CUE_PARSE_DEBUG*/ |
| 30 | |
| 31 | static void media_zero_trailing_spaces(char *buf, size_t len) |
| 32 | { |
| 33 | int i; |
| 34 | |
| 35 | for (i = len - 1; i >= 0; i--) |
| 36 | { |
| 37 | if (buf[i] == ' ') |
| 38 | buf[i] = '\0'; |
| 39 | else if (buf[i] != '\0') |
| 40 | break; |
| 41 | } |
| 42 | } |
| 43 | |
| 44 | static bool media_skip_spaces(const char **buf, size_t len) |
| 45 | { |
| 46 | bool found = false; |
| 47 | unsigned i; |
| 48 | |
| 49 | if (!buf || !*buf || !**buf) |
| 50 | return false; |
| 51 | |
| 52 | for (i = 0; i < len; i++) |
| 53 | { |
| 54 | if ((*buf)[i] == ' ' || (*buf)[i] == '\t') |
| 55 | continue; |
| 56 | |
| 57 | *buf += i; |
| 58 | found = true; |
| 59 | break; |
| 60 | } |
| 61 | |
| 62 | if (found) |
| 63 | return true; |
| 64 | |
| 65 | return false; |
| 66 | } |
| 67 | |
| 68 | /* Fill in "info" with detected CD info. Use this when you have a cue file and want it parsed to find the first data track and any pregap info. */ |
| 69 | bool media_detect_cd_info_cue(const char *path, media_detect_cd_info_t *info) |
| 70 | { |
| 71 | RFILE *file = NULL; |
| 72 | char *line = NULL; |
| 73 | char track_path[PATH_MAX_LENGTH] = {0}; |
| 74 | char track_abs_path[PATH_MAX_LENGTH] = {0}; |
| 75 | char track_mode[11] = {0}; |
| 76 | bool found_file = false; |
| 77 | bool found_track = false; |
| 78 | unsigned first_data_track = 0; |
| 79 | uint64_t data_track_pregap_bytes = 0; |
| 80 | |
| 81 | if (string_is_empty(path) || !info) |
| 82 | return false; |
| 83 | |
| 84 | file = filestream_open(path, RETRO_VFS_FILE_ACCESS_READ, 0); |
| 85 | |
| 86 | if (!file) |
| 87 | { |
| 88 | #ifdef MEDIA_CUE_PARSE_DEBUG |
| 89 | printf("[MEDIA] Could not open cue path for reading: %s\n", path); |
| 90 | fflush(stdout); |
| 91 | #endif |
| 92 | return false; |
| 93 | } |
| 94 | |
| 95 | while (!filestream_eof(file) && (line = filestream_getline(file))) |
| 96 | { |
| 97 | size_t len = 0; |
| 98 | const char *command = NULL; |
| 99 | |
| 100 | if (string_is_empty(line)) |
| 101 | { |
| 102 | free(line); |
| 103 | continue; |
| 104 | } |
| 105 | |
| 106 | len = strlen(line); |
| 107 | command = line; |
| 108 | |
| 109 | media_skip_spaces(&command, len); |
| 110 | |
| 111 | if (!found_file && !strncasecmp(command, "FILE", 4)) |
| 112 | { |
| 113 | const char *file = command + 4; |
| 114 | media_skip_spaces(&file, len - 4); |
| 115 | |
| 116 | if (!string_is_empty(file)) |
| 117 | { |
| 118 | const char *file_end = NULL; |
| 119 | size_t file_len = 0; |
| 120 | bool quoted = false; |
| 121 | |
| 122 | if (file[0] == '"') |
| 123 | { |
| 124 | quoted = true; |
| 125 | file++; |
| 126 | } |
| 127 | |
| 128 | if (quoted) |
| 129 | file_end = strchr(file, '\"'); |
| 130 | else |
| 131 | file_end = strchr(file, ' '); |
| 132 | |
| 133 | if (file_end) |
| 134 | { |
| 135 | file_len = file_end - file; |
| 136 | memcpy(track_path, file, file_len); |
| 137 | found_file = true; |
| 138 | #ifdef MEDIA_CUE_PARSE_DEBUG |
| 139 | printf("Found file: %s\n", track_path); |
| 140 | fflush(stdout); |
| 141 | #endif |
| 142 | } |
| 143 | } |
| 144 | } |
| 145 | else if (found_file && !found_track && !strncasecmp(command, "TRACK", 5)) |
| 146 | { |
| 147 | const char *track = command + 5; |
| 148 | media_skip_spaces(&track, len - 5); |
| 149 | |
| 150 | if (!string_is_empty(track)) |
| 151 | { |
| 152 | char *ptr = NULL; |
| 153 | unsigned track_number = (unsigned)strtol(track, &ptr, 10); |
| 154 | #ifdef MEDIA_CUE_PARSE_DEBUG |
| 155 | printf("Found track: %d\n", track_number); |
| 156 | fflush(stdout); |
| 157 | #endif |
| 158 | track++; |
| 159 | |
| 160 | if (track[0] && track[0] != ' ' && track[0] != '\t') |
| 161 | track++; |
| 162 | |
| 163 | if (!string_is_empty(track)) |
| 164 | { |
| 165 | media_skip_spaces(&track, strlen(track)); |
| 166 | #ifdef MEDIA_CUE_PARSE_DEBUG |
| 167 | printf("Found track type: %s\n", track); |
| 168 | fflush(stdout); |
| 169 | #endif |
| 170 | if (!strncasecmp(track, "MODE", 4)) |
| 171 | { |
| 172 | first_data_track = track_number; |
| 173 | found_track = true; |
| 174 | strlcpy(track_mode, track, sizeof(track_mode)); |
| 175 | } |
| 176 | else |
| 177 | found_file = false; |
| 178 | } |
| 179 | } |
| 180 | } |
| 181 | else if (found_file && found_track && first_data_track && !strncasecmp(command, "INDEX", 5)) |
| 182 | { |
| 183 | const char *index = command + 5; |
| 184 | media_skip_spaces(&index, len - 5); |
| 185 | |
| 186 | if (!string_is_empty(index)) |
| 187 | { |
| 188 | char *ptr = NULL; |
| 189 | unsigned index_number = (unsigned)strtol(index, &ptr, 10); |
| 190 | |
| 191 | if (index_number == 1) |
| 192 | { |
| 193 | const char *pregap = index + 1; |
| 194 | |
| 195 | if (pregap[0] && pregap[0] != ' ' && pregap[0] != '\t') |
| 196 | pregap++; |
| 197 | |
| 198 | if (!string_is_empty(pregap)) |
| 199 | { |
| 200 | media_skip_spaces(&pregap, strlen(pregap)); |
| 201 | found_file = false; |
| 202 | found_track = false; |
| 203 | |
| 204 | if (first_data_track && !string_is_empty(track_mode)) |
| 205 | { |
| 206 | unsigned track_sector_size = 0; |
| 207 | unsigned track_mode_number = 0; |
| 208 | |
| 209 | if (strlen(track_mode) == 10) |
| 210 | { |
| 211 | sscanf(track_mode, "MODE%d/%d", (int*)&track_mode_number, (int*)&track_sector_size); |
| 212 | #ifdef MEDIA_CUE_PARSE_DEBUG |
| 213 | printf("Found track mode %d with sector size %d\n", track_mode_number, track_sector_size); |
| 214 | fflush(stdout); |
| 215 | #endif |
| 216 | if ((track_mode_number == 1 || track_mode_number == 2) && track_sector_size) |
| 217 | { |
| 218 | unsigned min = 0; |
| 219 | unsigned sec = 0; |
| 220 | unsigned frame = 0; |
| 221 | sscanf(pregap, "%02d:%02d:%02d", (int*)&min, (int*)&sec, (int*)&frame); |
| 222 | |
| 223 | if (min || sec || frame || strstr(pregap, "00:00:00")) |
| 224 | { |
| 225 | data_track_pregap_bytes = ((min * 60 + sec) * 75 + frame) * track_sector_size; |
| 226 | #ifdef MEDIA_CUE_PARSE_DEBUG |
| 227 | printf("Found pregap of %02d:%02d:%02d (bytes: %" PRIu64 ")\n", min, sec, frame, data_track_pregap_bytes); |
| 228 | fflush(stdout); |
| 229 | #endif |
| 230 | break; |
| 231 | } |
| 232 | } |
| 233 | } |
| 234 | } |
| 235 | } |
| 236 | } |
| 237 | } |
| 238 | } |
| 239 | |
| 240 | free(line); |
| 241 | } |
| 242 | |
| 243 | filestream_close(file); |
| 244 | |
| 245 | if (!string_is_empty(track_path)) |
| 246 | { |
| 247 | if (strstr(track_path, "/") || strstr(track_path, "\\")) |
| 248 | { |
| 249 | #ifdef MEDIA_CUE_PARSE_DEBUG |
| 250 | printf("using path %s\n", track_path); |
| 251 | fflush(stdout); |
| 252 | #endif |
| 253 | return media_detect_cd_info(track_path, data_track_pregap_bytes, info); |
| 254 | } |
| 255 | |
| 256 | fill_pathname_basedir(track_abs_path, path, sizeof(track_abs_path)); |
| 257 | strlcat(track_abs_path, track_path, sizeof(track_abs_path)); |
| 258 | #ifdef MEDIA_CUE_PARSE_DEBUG |
| 259 | printf("using abs path %s\n", track_abs_path); |
| 260 | fflush(stdout); |
| 261 | #endif |
| 262 | return media_detect_cd_info(track_abs_path, data_track_pregap_bytes, info); |
| 263 | } |
| 264 | |
| 265 | return true; |
| 266 | } |
| 267 | |
| 268 | /* Fill in "info" with detected CD info. Use this when you want to open a specific track file directly, and the pregap is known. */ |
| 269 | bool media_detect_cd_info(const char *path, uint64_t pregap_bytes, media_detect_cd_info_t *info) |
| 270 | { |
| 271 | RFILE *file; |
| 272 | |
| 273 | if (string_is_empty(path) || !info) |
| 274 | return false; |
| 275 | |
| 276 | file = filestream_open(path, RETRO_VFS_FILE_ACCESS_READ, 0); |
| 277 | |
| 278 | if (!file) |
| 279 | { |
| 280 | #ifdef MEDIA_CUE_PARSE_DEBUG |
| 281 | printf("[MEDIA] Could not open path for reading: %s\n", path); |
| 282 | fflush(stdout); |
| 283 | #endif |
| 284 | return false; |
| 285 | } |
| 286 | |
| 287 | { |
| 288 | unsigned offset = 0; |
| 289 | unsigned sector_size = 0; |
| 290 | unsigned buf_size = 17 * 2352; |
| 291 | char *buf = (char*)calloc(1, buf_size); |
| 292 | int64_t read_bytes = 0; |
| 293 | |
| 294 | if (!buf) |
| 295 | return false; |
| 296 | |
| 297 | if (pregap_bytes) |
| 298 | filestream_seek(file, pregap_bytes, RETRO_VFS_SEEK_POSITION_START); |
| 299 | |
| 300 | read_bytes = filestream_read(file, buf, buf_size); |
| 301 | |
| 302 | if (read_bytes != buf_size) |
| 303 | { |
| 304 | #ifdef MEDIA_CUE_PARSE_DEBUG |
| 305 | printf("[MEDIA] Could not read from media: got %" PRId64 " bytes instead of %d.\n", read_bytes, buf_size); |
| 306 | fflush(stdout); |
| 307 | #endif |
| 308 | filestream_close(file); |
| 309 | free(buf); |
| 310 | return false; |
| 311 | } |
| 312 | |
| 313 | /* 12-byte sync field at the start of every sector, common to both mode1 and mode2 data tracks |
| 314 | * (when at least sync data is requested). This is a CD-ROM standard feature and not specific to any game devices, |
| 315 | * and as such should not be part of any system-specific detection or "magic" bytes. |
| 316 | * Depending on what parts of a sector were requested from the disc, the user data might start at |
| 317 | * byte offset 0, 4, 8, 12, 16 or 24. Cue sheets only specify the total number of bytes requested from the sectors |
| 318 | * of a track (like 2048 or 2352) and it is then assumed based on the size/mode as to what fields are present. */ |
| 319 | if (!memcmp(buf, "\x00\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x00", 12)) |
| 320 | { |
| 321 | /* Assume track data contains all fields. */ |
| 322 | sector_size = 2352; |
| 323 | |
| 324 | if (buf[15] == 2) |
| 325 | { |
| 326 | /* assume Mode 2 formed (formless is rarely used) */ |
| 327 | offset = 24; |
| 328 | } |
| 329 | else |
| 330 | { |
| 331 | /* assume Mode 1 */ |
| 332 | offset = 16; |
| 333 | } |
| 334 | } |
| 335 | else |
| 336 | { |
| 337 | /* Assume sectors only contain user data instead. */ |
| 338 | offset = 0; |
| 339 | sector_size = 2048; |
| 340 | } |
| 341 | |
| 342 | if (!memcmp(buf + offset, "SEGADISCSYSTEM", |
| 343 | STRLEN_CONST("SEGADISCSYSTEM"))) |
| 344 | { |
| 345 | const char *title_pos = NULL; |
| 346 | const char *serial_pos = NULL; |
| 347 | |
| 348 | /* All discs currently in Redump for MCD start with SEGADISCSYSTEM. There are other strings mentioned elsewhere online, |
| 349 | * but I have not seen any real examples of them. */ |
| 350 | info->system_id = MEDIA_CD_SYSTEM_MEGA_CD; |
| 351 | |
| 352 | strcpy_literal(info->system, "Sega CD / Mega CD"); |
| 353 | |
| 354 | title_pos = buf + offset + 0x150; |
| 355 | |
| 356 | if (media_skip_spaces(&title_pos, 48)) |
| 357 | { |
| 358 | memcpy(info->title, title_pos, 48 - (title_pos - (buf + offset + 0x150))); |
| 359 | media_zero_trailing_spaces(info->title, sizeof(info->title)); |
| 360 | } |
| 361 | else |
| 362 | { |
| 363 | info->title[0] = 'N'; |
| 364 | info->title[1] = '/'; |
| 365 | info->title[2] = 'A'; |
| 366 | info->title[3] = '\0'; |
| 367 | } |
| 368 | |
| 369 | serial_pos = buf + offset + 0x183; |
| 370 | |
| 371 | if (media_skip_spaces(&serial_pos, 8)) |
| 372 | { |
| 373 | memcpy(info->serial, serial_pos, 8 - (serial_pos - (buf + offset + 0x183))); |
| 374 | media_zero_trailing_spaces(info->serial, sizeof(info->serial)); |
| 375 | } |
| 376 | else |
| 377 | { |
| 378 | info->serial[0] = 'N'; |
| 379 | info->serial[1] = '/'; |
| 380 | info->serial[2] = 'A'; |
| 381 | info->serial[3] = '\0'; |
| 382 | } |
| 383 | } |
| 384 | else if (!memcmp(buf + offset, "SEGA SEGASATURN", |
| 385 | STRLEN_CONST("SEGA SEGASATURN"))) |
| 386 | { |
| 387 | const char *title_pos = NULL; |
| 388 | const char *serial_pos = NULL; |
| 389 | const char *version_pos = NULL; |
| 390 | const char *release_date_pos = NULL; |
| 391 | |
| 392 | info->system_id = MEDIA_CD_SYSTEM_SATURN; |
| 393 | |
| 394 | strcpy_literal(info->system, "Sega Saturn"); |
| 395 | |
| 396 | title_pos = buf + offset + 0x60; |
| 397 | |
| 398 | if (media_skip_spaces(&title_pos, 112)) |
| 399 | { |
| 400 | memcpy(info->title, title_pos, 112 - (title_pos - (buf + offset + 0x60))); |
| 401 | media_zero_trailing_spaces(info->title, sizeof(info->title)); |
| 402 | } |
| 403 | else |
| 404 | { |
| 405 | info->title [0] = 'N'; |
| 406 | info->title [1] = '/'; |
| 407 | info->title [2] = 'A'; |
| 408 | info->title [3] = '\0'; |
| 409 | } |
| 410 | |
| 411 | serial_pos = buf + offset + 0x20; |
| 412 | |
| 413 | if (media_skip_spaces(&serial_pos, 10)) |
| 414 | { |
| 415 | memcpy(info->serial, serial_pos, 10 - (serial_pos - (buf + offset + 0x20))); |
| 416 | media_zero_trailing_spaces(info->serial, sizeof(info->serial)); |
| 417 | } |
| 418 | else |
| 419 | { |
| 420 | info->serial[0] = 'N'; |
| 421 | info->serial[1] = '/'; |
| 422 | info->serial[2] = 'A'; |
| 423 | info->serial[3] = '\0'; |
| 424 | } |
| 425 | |
| 426 | version_pos = buf + offset + 0x2a; |
| 427 | |
| 428 | if (media_skip_spaces(&version_pos, 6)) |
| 429 | { |
| 430 | memcpy(info->version, version_pos, 6 - (version_pos - (buf + offset + 0x2a))); |
| 431 | media_zero_trailing_spaces(info->version, sizeof(info->version)); |
| 432 | } |
| 433 | else |
| 434 | { |
| 435 | info->version[0] = 'N'; |
| 436 | info->version[1] = '/'; |
| 437 | info->version[2] = 'A'; |
| 438 | info->version[3] = '\0'; |
| 439 | } |
| 440 | |
| 441 | release_date_pos = buf + offset + 0x30; |
| 442 | |
| 443 | if (media_skip_spaces(&release_date_pos, 8)) |
| 444 | { |
| 445 | memcpy(info->release_date, release_date_pos, 8 - (release_date_pos - (buf + offset + 0x30))); |
| 446 | media_zero_trailing_spaces(info->release_date, sizeof(info->release_date)); |
| 447 | } |
| 448 | else |
| 449 | { |
| 450 | info->release_date[0] = 'N'; |
| 451 | info->release_date[1] = '/'; |
| 452 | info->release_date[2] = 'A'; |
| 453 | info->release_date[3] = '\0'; |
| 454 | } |
| 455 | } |
| 456 | else if (!memcmp(buf + offset, "SEGA SEGAKATANA", STRLEN_CONST("SEGA SEGAKATANA"))) |
| 457 | { |
| 458 | const char *title_pos = NULL; |
| 459 | const char *serial_pos = NULL; |
| 460 | const char *version_pos = NULL; |
| 461 | const char *release_date_pos = NULL; |
| 462 | |
| 463 | info->system_id = MEDIA_CD_SYSTEM_DREAMCAST; |
| 464 | |
| 465 | strcpy_literal(info->system, "Sega Dreamcast"); |
| 466 | |
| 467 | title_pos = buf + offset + 0x80; |
| 468 | |
| 469 | if (media_skip_spaces(&title_pos, 96)) |
| 470 | { |
| 471 | memcpy(info->title, title_pos, 96 - (title_pos - (buf + offset + 0x80))); |
| 472 | media_zero_trailing_spaces(info->title, sizeof(info->title)); |
| 473 | } |
| 474 | else |
| 475 | { |
| 476 | info->title [0] = 'N'; |
| 477 | info->title [1] = '/'; |
| 478 | info->title [2] = 'A'; |
| 479 | info->title [3] = '\0'; |
| 480 | } |
| 481 | |
| 482 | serial_pos = buf + offset + 0x40; |
| 483 | |
| 484 | if (media_skip_spaces(&serial_pos, 10)) |
| 485 | { |
| 486 | memcpy(info->serial, serial_pos, 10 - (serial_pos - (buf + offset + 0x40))); |
| 487 | media_zero_trailing_spaces(info->serial, sizeof(info->serial)); |
| 488 | } |
| 489 | else |
| 490 | { |
| 491 | info->serial [0] = 'N'; |
| 492 | info->serial [1] = '/'; |
| 493 | info->serial [2] = 'A'; |
| 494 | info->serial [3] = '\0'; |
| 495 | } |
| 496 | |
| 497 | version_pos = buf + offset + 0x4a; |
| 498 | |
| 499 | if (media_skip_spaces(&version_pos, 6)) |
| 500 | { |
| 501 | memcpy(info->version, version_pos, 6 - (version_pos - (buf + offset + 0x4a))); |
| 502 | media_zero_trailing_spaces(info->version, sizeof(info->version)); |
| 503 | } |
| 504 | else |
| 505 | { |
| 506 | info->version [0] = 'N'; |
| 507 | info->version [1] = '/'; |
| 508 | info->version [2] = 'A'; |
| 509 | info->version [3] = '\0'; |
| 510 | } |
| 511 | |
| 512 | release_date_pos = buf + offset + 0x50; |
| 513 | |
| 514 | if (media_skip_spaces(&release_date_pos, 8)) |
| 515 | { |
| 516 | memcpy(info->release_date, release_date_pos, 8 - (release_date_pos - (buf + offset + 0x50))); |
| 517 | media_zero_trailing_spaces(info->release_date, sizeof(info->release_date)); |
| 518 | } |
| 519 | else |
| 520 | { |
| 521 | info->release_date[0] = 'N'; |
| 522 | info->release_date[1] = '/'; |
| 523 | info->release_date[2] = 'A'; |
| 524 | info->release_date[3] = '\0'; |
| 525 | } |
| 526 | } |
| 527 | /* Primary Volume Descriptor fields of ISO9660 */ |
| 528 | else if (!memcmp(buf + offset + (16 * sector_size), "\1CD001\1\0PLAYSTATION", 19)) |
| 529 | { |
| 530 | const char *title_pos = NULL; |
| 531 | |
| 532 | info->system_id = MEDIA_CD_SYSTEM_PSX; |
| 533 | |
| 534 | strcpy_literal(info->system, "Sony PlayStation"); |
| 535 | |
| 536 | title_pos = buf + offset + (16 * sector_size) + 40; |
| 537 | |
| 538 | if (media_skip_spaces(&title_pos, 32)) |
| 539 | { |
| 540 | memcpy(info->title, title_pos, 32 - (title_pos - (buf + offset + (16 * sector_size) + 40))); |
| 541 | media_zero_trailing_spaces(info->title, sizeof(info->title)); |
| 542 | } |
| 543 | else |
| 544 | { |
| 545 | info->title [0] = 'N'; |
| 546 | info->title [1] = '/'; |
| 547 | info->title [2] = 'A'; |
| 548 | info->title [3] = '\0'; |
| 549 | } |
| 550 | } |
| 551 | else if (!memcmp(buf + offset, "\x01\x5a\x5a\x5a\x5a\x5a\x01\x00\x00\x00\x00\x00", 12)) |
| 552 | { |
| 553 | info->system_id = MEDIA_CD_SYSTEM_3DO; |
| 554 | |
| 555 | strcpy_literal(info->system, "3DO"); |
| 556 | } |
| 557 | else if (!memcmp(buf + offset + 0x950, "PC Engine CD-ROM SYSTEM", 23)) |
| 558 | { |
| 559 | info->system_id = MEDIA_CD_SYSTEM_PC_ENGINE_CD; |
| 560 | |
| 561 | strcpy_literal(info->system, "TurboGrafx-CD / PC-Engine CD"); |
| 562 | } |
| 563 | |
| 564 | free(buf); |
| 565 | } |
| 566 | |
| 567 | filestream_close(file); |
| 568 | |
| 569 | return true; |
| 570 | } |