mirror of
https://github.com/MiSTer-devel/Main_MiSTer.git
synced 2026-04-12 03:04:02 +00:00
1764 lines
49 KiB
C++
1764 lines
49 KiB
C++
#include <ctype.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
|
|
#include "../../hardware.h"
|
|
#include "../../menu.h"
|
|
#include "../../shmem.h"
|
|
#include "../../lib/md5/md5.h"
|
|
|
|
#include "miniz.h"
|
|
#include "n64.h"
|
|
#include "n64_cpak_header.h"
|
|
|
|
#pragma push_macro("NONE")
|
|
#pragma push_macro("BIG_ENDIAN")
|
|
#pragma push_macro("LITTLE_ENDIAN")
|
|
#undef NONE
|
|
#undef BIG_ENDIAN
|
|
#undef LITTLE_ENDIAN
|
|
|
|
static constexpr auto RAM_SIZE = 0x800000U;
|
|
static constexpr auto CARTID_LENGTH = 6U; // Ex: NSME00
|
|
static constexpr auto MD5_LENGTH = 16U;
|
|
static constexpr auto CARTID_PREFIX = "ID:";
|
|
static constexpr auto RESET_OPT = "[0]";
|
|
static constexpr auto RELOAD_SAVE_OPT = "[40]";
|
|
static constexpr auto AUTODETECT_OPT = "[64]";
|
|
static constexpr auto CIC_TYPE_OPT = "[68:65]";
|
|
static constexpr auto NO_EPAK_OPT = "[70]";
|
|
static constexpr auto CPAK_OPT = "[71]";
|
|
static constexpr auto RPAK_OPT = "[72]";
|
|
static constexpr auto TPAK_OPT = "[73]";
|
|
static constexpr auto RTC_OPT = "[74]";
|
|
static constexpr auto SAVE_TYPE_OPT = "[77:75]";
|
|
static constexpr auto SYS_TYPE_OPT = "[80:79]";
|
|
static constexpr auto AUTOPAK_OPT = "[81]";
|
|
static constexpr auto PATCHES_OPT = "[90]";
|
|
static constexpr auto CHEATS_OPT = "[103]";
|
|
static constexpr const char* const CONTROLLER_OPTS[] = { "[51:49]", "[54:52]", "[57:55]", "[60:58]" };
|
|
|
|
// Simple hash function, see: https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function
|
|
// (Modified to make it case-insensitive)
|
|
static constexpr uint64_t FNV_PRIME = 0x100000001b3;
|
|
static constexpr uint64_t FNV_OFFSET_BASIS = 0xcbf29ce484222325;
|
|
static constexpr uint64_t fnv_hash(const char* s, uint64_t h = FNV_OFFSET_BASIS) {
|
|
if (s) while (uint8_t a = *(s++)) h = (h ^ ((a >= 'A') && (a <= 'Z') ? a + ('a' - 'A') : a)) * FNV_PRIME;
|
|
return h;
|
|
}
|
|
|
|
enum class MemoryType : uint32_t {
|
|
NONE = 0,
|
|
EEPROM_512,
|
|
EEPROM_2k,
|
|
SRAM_32k,
|
|
SRAM_96k,
|
|
FLASH_128k,
|
|
CPAK = ~2U,
|
|
TPAK = ~1U,
|
|
UNKNOWN = ~0U
|
|
};
|
|
|
|
enum class CIC : uint32_t {
|
|
CIC_NUS_6101 = 0,
|
|
CIC_NUS_6102,
|
|
CIC_NUS_7101,
|
|
CIC_NUS_7102,
|
|
CIC_NUS_6103,
|
|
CIC_NUS_7103,
|
|
CIC_NUS_6105,
|
|
CIC_NUS_7105,
|
|
CIC_NUS_6106,
|
|
CIC_NUS_7106,
|
|
CIC_NUS_8303,
|
|
CIC_NUS_8401,
|
|
CIC_NUS_5167,
|
|
CIC_NUS_DDUS,
|
|
CIC_NUS_5101,
|
|
UNKNOWN = ~0U
|
|
};
|
|
|
|
enum class SystemType : uint32_t {
|
|
NTSC = 0,
|
|
PAL,
|
|
UNKNOWN = ~0U
|
|
};
|
|
|
|
enum class ByteOrder : uint32_t {
|
|
BIG_ENDIAN = 0,
|
|
BYTE_SWAPPED,
|
|
LITTLE_ENDIAN,
|
|
UNKNOWN = ~0U
|
|
};
|
|
|
|
enum class PadType : uint32_t {
|
|
N64_PAD = 0,
|
|
UNPLUGGED,
|
|
N64_PAD_WITH_CPAK,
|
|
N64_PAD_WITH_RPAK,
|
|
SNAC,
|
|
N64_PAD_WITH_TPAK,
|
|
UNKNOWN = ~0U
|
|
};
|
|
|
|
enum class AutoDetect : uint32_t {
|
|
ON = 0,
|
|
OFF
|
|
};
|
|
|
|
enum class ComparisionType : uint8_t {
|
|
OPTYPE_ALWAYS = 0,
|
|
OPTYPE_EQUALS = 0x1,
|
|
OPTYPE_GREATER = 0x2,
|
|
OPTYPE_LESS = 0x3,
|
|
OPTYPE_GREATER_EQ = 0x4,
|
|
OPTYPE_LESS_EQ = 0x5,
|
|
OPTYPE_NOT_EQ = 0x6,
|
|
OPTYPE_EMPTY = 0xf
|
|
};
|
|
|
|
struct cheat_code_flags {
|
|
enum ComparisionType cmp_type : 4;
|
|
uint8_t cmp_mask : 4;
|
|
bool is_boot_code : 1;
|
|
bool is_boot_code_executed : 1;
|
|
bool is_gs_button_code : 1;
|
|
bool is_gs_button_pressed : 1;
|
|
uint32_t _unused : 20;
|
|
} __attribute__((packed));
|
|
|
|
struct cheat_code {
|
|
struct cheat_code_flags flags;
|
|
uint32_t address;
|
|
uint32_t compare;
|
|
uint32_t replace;
|
|
};
|
|
|
|
struct patch_data {
|
|
uint32_t address;
|
|
uint32_t diff;
|
|
bool turbo_core_only;
|
|
};
|
|
|
|
static uint8_t loaded = 0;
|
|
static char current_rom_path[1024] = { '\0' };
|
|
static char current_rom_path_gb[1024] = { '\0' };
|
|
static char old_save_path[1024];
|
|
static void* rdram_ptr = nullptr;
|
|
static cheat_code* cheat_codes = nullptr;
|
|
static uint32_t cheat_codes_count = 0;
|
|
static patch_data patches[256];
|
|
static int is_turbo_core_type = 0;
|
|
|
|
static const char* stringify(MemoryType v) {
|
|
switch (v) {
|
|
case MemoryType::EEPROM_512: return "4K EEPROM";
|
|
case MemoryType::EEPROM_2k: return "16K EEPROM";
|
|
case MemoryType::SRAM_32k: return "256K SRAM";
|
|
case MemoryType::SRAM_96k: return "768K SRAM";
|
|
case MemoryType::FLASH_128k: return "Flash RAM";
|
|
case MemoryType::CPAK: return "CPAK DATA";
|
|
case MemoryType::TPAK: return "TPAK DATA";
|
|
default: return "(none)";
|
|
}
|
|
}
|
|
|
|
static size_t get_save_size(MemoryType v) {
|
|
switch (v) {
|
|
case MemoryType::EEPROM_512: return 0x200;
|
|
case MemoryType::EEPROM_2k: return 0x800;
|
|
case MemoryType::SRAM_32k: return 0x8000;
|
|
case MemoryType::SRAM_96k: return 0x18000;
|
|
case MemoryType::FLASH_128k: return 0x20000;
|
|
case MemoryType::CPAK:
|
|
case MemoryType::TPAK: return 0x8000; // 32 KiByte
|
|
default: return 0;
|
|
}
|
|
}
|
|
|
|
static const char* stringify(CIC v) {
|
|
switch (v) {
|
|
case CIC::CIC_NUS_6101: return "6101";
|
|
case CIC::CIC_NUS_6102: return "6102";
|
|
case CIC::CIC_NUS_7101: return "7101";
|
|
case CIC::CIC_NUS_7102: return "7102";
|
|
case CIC::CIC_NUS_6103: return "6103";
|
|
case CIC::CIC_NUS_7103: return "7103";
|
|
case CIC::CIC_NUS_6105: return "6105";
|
|
case CIC::CIC_NUS_7105: return "7105";
|
|
case CIC::CIC_NUS_6106: return "6106";
|
|
case CIC::CIC_NUS_7106: return "7106";
|
|
case CIC::CIC_NUS_8303: return "8303";
|
|
case CIC::CIC_NUS_8401: return "8401";
|
|
case CIC::CIC_NUS_5167: return "5167";
|
|
case CIC::CIC_NUS_DDUS: return "DDUS";
|
|
case CIC::CIC_NUS_5101: return "5101";
|
|
default: return "Unknown";
|
|
}
|
|
}
|
|
|
|
static const char* stringify(SystemType v) {
|
|
switch (v) {
|
|
case SystemType::NTSC: return "NTSC";
|
|
case SystemType::PAL: return "PAL";
|
|
default: return "Unknown";
|
|
}
|
|
}
|
|
|
|
static const char* stringify(bool v) {
|
|
return v
|
|
? "Yes"
|
|
: "No";
|
|
}
|
|
|
|
static ByteOrder detect_rom_endianness(const uint8_t* data) {
|
|
// Data should be aligned
|
|
const uint32_t val = *(uint32_t*)data;
|
|
|
|
/* The following checks assume we're on a little-endian platform.
|
|
For each check, the first value is for regular ROMs, the 2nd is for 64DD images and
|
|
the third is a malformed "word" used in some homebrew(?) */
|
|
switch (val) {
|
|
case UINT32_C(0x40123780):
|
|
case UINT32_C(0x40072780):
|
|
case UINT32_C(0x41123780):
|
|
return ByteOrder::BIG_ENDIAN;
|
|
case UINT32_C(0x12408037):
|
|
case UINT32_C(0x07408027):
|
|
case UINT32_C(0x12418037):
|
|
return ByteOrder::BYTE_SWAPPED;
|
|
case UINT32_C(0x80371240):
|
|
case UINT32_C(0x80270740):
|
|
case UINT32_C(0x80371241):
|
|
return ByteOrder::LITTLE_ENDIAN;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// Endianness could not be determined, use just first byte and hope for the best.
|
|
switch (val & 0xff) {
|
|
case 0x80:
|
|
return ByteOrder::BIG_ENDIAN;
|
|
case 0x37:
|
|
case 0x27:
|
|
return ByteOrder::BYTE_SWAPPED;
|
|
case 0x40:
|
|
case 0x41:
|
|
return ByteOrder::LITTLE_ENDIAN;
|
|
default:
|
|
return ByteOrder::UNKNOWN;
|
|
}
|
|
}
|
|
|
|
static void normalize_data(uint8_t* data, size_t size, ByteOrder endianness) {
|
|
static uint8_t temp0, temp1, temp2;
|
|
switch (endianness) {
|
|
case ByteOrder::BYTE_SWAPPED:
|
|
for (size_t i = 0; i < (size & ~1U); i += 2) {
|
|
temp0 = data[0];
|
|
data[0] = data[1];
|
|
data[1] = temp0;
|
|
data += 2;
|
|
}
|
|
break;
|
|
case ByteOrder::LITTLE_ENDIAN:
|
|
for (size_t i = 0; i < (size & ~3U); i += 4) {
|
|
temp0 = data[0];
|
|
temp1 = data[1];
|
|
temp2 = data[2];
|
|
data[0] = data[3];
|
|
data[1] = temp2;
|
|
data[2] = temp1;
|
|
data[3] = temp0;
|
|
data += 4;
|
|
}
|
|
break;
|
|
default:
|
|
// Do nothing
|
|
break;
|
|
}
|
|
}
|
|
|
|
static MemoryType get_cart_save_type() {
|
|
auto v = (MemoryType)user_io_status_get(SAVE_TYPE_OPT);
|
|
return (get_save_size(v) ? v : MemoryType::NONE);
|
|
}
|
|
|
|
static void set_cart_save_type(MemoryType v) {
|
|
user_io_status_set(SAVE_TYPE_OPT,
|
|
(uint32_t)(get_save_size(v) ? v : MemoryType::NONE));
|
|
}
|
|
|
|
static uint32_t get_save_offset(unsigned char idx) {
|
|
uint32_t offset = 0;
|
|
MemoryType save_type = get_cart_save_type();
|
|
|
|
if (idx && (save_type != MemoryType::NONE)) {
|
|
offset += get_save_size(save_type);
|
|
idx--;
|
|
}
|
|
|
|
if (idx && (bool)user_io_status_get(TPAK_OPT)) {
|
|
offset += get_save_size(MemoryType::TPAK);
|
|
idx--;
|
|
}
|
|
|
|
offset += get_save_size(MemoryType::CPAK) * idx;
|
|
|
|
return offset;
|
|
}
|
|
|
|
static char full_path[1024];
|
|
static uint8_t save_file_buf[0x20000]; // Largest save size
|
|
static uint8_t mounted_save_files = 0;
|
|
|
|
static size_t create_file(const char* filename, const uint8_t* data, size_t sz) {
|
|
snprintf(full_path, sizeof(full_path), "%s/%s", getRootDir(), filename);
|
|
FILE* fp = fopen(full_path, "w");
|
|
if (!fp) return 0;
|
|
sz = fwrite(data, 1, sz, fp);
|
|
fclose(fp);
|
|
return sz;
|
|
}
|
|
|
|
static size_t read_file(const char* filename, uint8_t* data, uint32_t offset, size_t sz) {
|
|
snprintf(full_path, sizeof(full_path), "%s/%s", getRootDir(), filename);
|
|
FILE* fp = fopen(full_path, "r");
|
|
if (!fp) return 0;
|
|
fseek(fp, 0L, SEEK_END);
|
|
size_t fs = ftell(fp);
|
|
if (fs > sizeof(save_file_buf)) {
|
|
fs = sizeof(save_file_buf);
|
|
}
|
|
if (offset >= fs) {
|
|
fclose(fp);
|
|
return 0;
|
|
}
|
|
if (offset + sz > fs) {
|
|
sz = fs - offset;
|
|
}
|
|
fseek(fp, offset, SEEK_SET);
|
|
sz = fread(data, 1, sz, fp);
|
|
fclose(fp);
|
|
|
|
return sz;
|
|
}
|
|
|
|
static bool is_empty(uint8_t* arr, size_t sz) {
|
|
for (; sz-- > 0; arr++) {
|
|
if (*arr) return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Shouldn't be possible to allocate more than 5 save files right now...
|
|
static N64SaveFile* save_files[8] = { nullptr };
|
|
|
|
struct N64SaveFile {
|
|
int idx;
|
|
MemoryType type;
|
|
|
|
fileTYPE* get_image() const {
|
|
return ((this->idx >= 0) && (this->idx < (int)(sizeof(save_files) / sizeof(*save_files)))) ? (fileTYPE*)::get_image(this->idx) : nullptr;
|
|
}
|
|
|
|
bool is_mounted() const {
|
|
fileTYPE* image;
|
|
return (image = this->get_image()) && image->filp;
|
|
}
|
|
|
|
size_t get_size() const {
|
|
return is_mounted() ? (size_t)this->get_image()->size : get_save_size(this->type);
|
|
}
|
|
|
|
// Return "true" if target is fine
|
|
bool copy_file(const char* path, const char* to_path, bool overwrite, bool resize = false) const {
|
|
if (!FileExists(path, 0)) {
|
|
return false;
|
|
}
|
|
|
|
size_t expected_sz = get_save_size(this->type);
|
|
size_t sz = read_file(path, save_file_buf, 0, resize ? expected_sz : sizeof(save_file_buf));
|
|
|
|
if (FileExists(to_path, 0) && (!overwrite || is_empty(save_file_buf, sz))) {
|
|
return true;
|
|
}
|
|
|
|
if (resize && (sz < expected_sz)) {
|
|
// Pad file
|
|
memset(save_file_buf + sz, 0, expected_sz - sz);
|
|
sz = expected_sz;
|
|
}
|
|
|
|
if (!create_file(to_path, save_file_buf, sz)) {
|
|
return false;
|
|
}
|
|
|
|
printf("Copied save data \"%s\" to \"%s\". (%u bytes)\n", path, to_path, sz);
|
|
return true;
|
|
}
|
|
|
|
// Return "true" if created
|
|
bool create_if_missing(const char* path, const char* old_path) {
|
|
if (FileExists(path, 0)) {
|
|
return false;
|
|
}
|
|
|
|
auto sz = this->get_size();
|
|
memset(save_file_buf, 0, sz);
|
|
bool found_old_data = false;
|
|
|
|
if (sz && FileExists(old_path, 0)) {
|
|
uint32_t off = get_save_offset((this->idx < 0) ? mounted_save_files : this->idx);
|
|
if (read_file(old_path, save_file_buf, off, sz)) {
|
|
printf("Found old save data \"%s\", converting to %s.\n", old_path, stringify(type));
|
|
found_old_data = true;
|
|
if ((this->type == MemoryType::CPAK) || (this->type == MemoryType::TPAK)) {
|
|
normalize_data(save_file_buf, sz, ByteOrder::LITTLE_ENDIAN);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!found_old_data && (this->type == MemoryType::CPAK)) {
|
|
memcpy(save_file_buf, cpak_header[((this->idx < 0) ? mounted_save_files : idx) % (sizeof(cpak_header) / sizeof(*cpak_header))], sizeof(*cpak_header));
|
|
}
|
|
|
|
return create_file(path, save_file_buf, sz);
|
|
}
|
|
|
|
void mount(const char* path) {
|
|
int pre_size = get_save_size(this->type);
|
|
if (!pre_size) return;
|
|
uint8_t idx = mounted_save_files++;
|
|
if (idx >= (sizeof(save_files) / sizeof(*save_files))) return;
|
|
if (save_files[idx] && save_files[idx]->is_mounted()) {
|
|
save_files[idx]->unmount();
|
|
delete save_files[idx];
|
|
save_files[idx] = nullptr;
|
|
}
|
|
user_io_file_mount(path, idx, 1, pre_size);
|
|
this->idx = idx;
|
|
save_files[idx] = this;
|
|
}
|
|
|
|
void mount(const char* path, const char* old_path) {
|
|
this->create_if_missing(path, old_path);
|
|
this->mount(path);
|
|
}
|
|
|
|
void unmount() {
|
|
if (!this->is_mounted()) return;
|
|
printf("Unmounting %s save file at %d slot.\n", stringify(this->type), this->idx);
|
|
user_io_file_mount("", this->idx);
|
|
}
|
|
|
|
N64SaveFile(MemoryType type) {
|
|
this->idx = -1; // Unmounted
|
|
this->type = type;
|
|
}
|
|
};
|
|
|
|
static bool is_auto() {
|
|
return (AutoDetect)user_io_status_get(AUTODETECT_OPT) == AutoDetect::ON;
|
|
}
|
|
|
|
static bool is_autopak() {
|
|
return !user_io_status_get(AUTOPAK_OPT);
|
|
}
|
|
|
|
static bool is_turbo_core() {
|
|
if (!is_turbo_core_type) {
|
|
is_turbo_core_type = 2;
|
|
|
|
user_io_read_confstr();
|
|
for (size_t i = 2; ; i++) {
|
|
char *p = user_io_get_confstr(i);
|
|
if (!p) break;
|
|
if (strncasecmp(p, "TURBO", 5) == 0) {
|
|
is_turbo_core_type = 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return is_turbo_core_type == 1;
|
|
}
|
|
|
|
static bool cheats_enabled() {
|
|
return !user_io_status_get(CHEATS_OPT);
|
|
}
|
|
|
|
static void cheats_disable() {
|
|
user_io_status_set(CHEATS_OPT, 1);
|
|
}
|
|
|
|
static bool patches_enabled() {
|
|
return !user_io_status_get(PATCHES_OPT);
|
|
}
|
|
|
|
static uint8_t hex_to_dec(const char x) {
|
|
if (x >= '0' && x <= '9') return (x - '0');
|
|
if (x >= 'A' && x <= 'F') return (x - 'A') + 10;
|
|
if (x >= 'a' && x <= 'f') return (x - 'a') + 10;
|
|
return 0;
|
|
}
|
|
|
|
static void trim(char* out, size_t max_len, const char* str)
|
|
{
|
|
if (!*str || !max_len) {
|
|
*out = '\0';
|
|
return;
|
|
}
|
|
|
|
const char* end;
|
|
size_t out_size = max_len;
|
|
|
|
// Trim leading space
|
|
while (isspace(*str)) {
|
|
if (!(--out_size)) {
|
|
*out = '\0';
|
|
return;
|
|
}
|
|
str++;
|
|
}
|
|
|
|
// All spaces?
|
|
if (!*str) {
|
|
*out = '\0';
|
|
return;
|
|
}
|
|
|
|
// Trim trailing space
|
|
end = str + strnlen(str, max_len) - 1;
|
|
while (end > str && isspace(*end)) {
|
|
end--;
|
|
}
|
|
end++;
|
|
|
|
// Set output size to minimum of trimmed string length and buffer size minus 1
|
|
out_size = (size_t)(end - str) < (max_len - 1) ? (end - str) : max_len - 1;
|
|
|
|
// Copy trimmed string and add null terminator
|
|
memcpy(out, str, out_size);
|
|
out[out_size] = '\0';
|
|
|
|
// Obfuscate illegal characters
|
|
for (size_t i = 0; (i < out_size) && out[i]; i++) {
|
|
if ((out[i] >= 0x20) && (out[i] < 0xa0)) {
|
|
continue;
|
|
}
|
|
out[i] = '?';
|
|
}
|
|
}
|
|
|
|
// Returns true if CIC and System Region is detected, or if auto-detection is turned off
|
|
static bool parse_and_apply_db_tags(char* tags) {
|
|
if (!tags) return false;
|
|
|
|
const char* separator = "| ";
|
|
MemoryType save_type = MemoryType::NONE;
|
|
SystemType system_type = SystemType::UNKNOWN;
|
|
CIC cic_type = CIC::UNKNOWN;
|
|
bool no_epak = false;
|
|
bool cpak = false;
|
|
bool rpak = false;
|
|
bool tpak = false;
|
|
bool rtc = false;
|
|
int p_match;
|
|
uint32_t p_addr, p_diff;
|
|
char p_tag;
|
|
|
|
PadType prefered_pad = PadType::N64_PAD;
|
|
|
|
patch_data* patch = patches;
|
|
|
|
for (char* tag = strtok(tags, separator); tag; tag = strtok(nullptr, separator)) {
|
|
// Patch tag
|
|
if ((p_match = sscanf(tag, "%*[Pp]%8x:%8x%*[:]%c", &p_addr, &p_diff, &p_tag)) >= 2) {
|
|
printf("Patch: 0x%08x:0x%08x", p_addr, p_diff);
|
|
if (p_match >= 3) printf(":%c", p_tag);
|
|
printf("\n");
|
|
patch->address = p_addr;
|
|
patch->diff = p_diff;
|
|
patch->turbo_core_only = p_match >= 3 && (p_tag == 'T' || p_tag == 't');
|
|
patch++;
|
|
continue;
|
|
}
|
|
|
|
switch (fnv_hash(tag)) {
|
|
case fnv_hash("eeprom512"): save_type = MemoryType::EEPROM_512; break;
|
|
case fnv_hash("eeprom2k"): save_type = MemoryType::EEPROM_2k; break;
|
|
case fnv_hash("sram32k"): save_type = MemoryType::SRAM_32k; break;
|
|
case fnv_hash("sram96k"): save_type = MemoryType::SRAM_96k; break;
|
|
case fnv_hash("flash128k"): save_type = MemoryType::FLASH_128k; break;
|
|
case fnv_hash("noepak"): no_epak = true; break;
|
|
case fnv_hash("cpak"): cpak = true; if (prefered_pad == PadType::N64_PAD) prefered_pad = PadType::N64_PAD_WITH_CPAK; break;
|
|
case fnv_hash("rpak"): rpak = true; if (prefered_pad == PadType::N64_PAD) prefered_pad = PadType::N64_PAD_WITH_RPAK; break;
|
|
case fnv_hash("tpak"): tpak = true; if (prefered_pad == PadType::N64_PAD) prefered_pad = PadType::N64_PAD_WITH_TPAK; break;
|
|
case fnv_hash("rtc"): rtc = true; break;
|
|
case fnv_hash("ntsc"): ntsc: system_type = SystemType::NTSC; break;
|
|
case fnv_hash("pal"): pal: system_type = SystemType::PAL; break;
|
|
case fnv_hash("cic6101"): cic_type = CIC::CIC_NUS_6101; goto ntsc;
|
|
case fnv_hash("cic6102"): cic_type = CIC::CIC_NUS_6102; goto ntsc;
|
|
case fnv_hash("cic6103"): cic_type = CIC::CIC_NUS_6103; goto ntsc;
|
|
case fnv_hash("cic6105"): cic_type = CIC::CIC_NUS_6105; goto ntsc;
|
|
case fnv_hash("cic6106"): cic_type = CIC::CIC_NUS_6106; goto ntsc;
|
|
case fnv_hash("cic7101"): cic_type = CIC::CIC_NUS_7101; goto pal;
|
|
case fnv_hash("cic7102"): cic_type = CIC::CIC_NUS_7102; goto pal;
|
|
case fnv_hash("cic7103"): cic_type = CIC::CIC_NUS_7103; goto pal;
|
|
case fnv_hash("cic7105"): cic_type = CIC::CIC_NUS_7105; goto pal;
|
|
case fnv_hash("cic7106"): cic_type = CIC::CIC_NUS_7106; goto pal;
|
|
case fnv_hash("cic8303"): cic_type = CIC::CIC_NUS_8303; break;
|
|
case fnv_hash("cic8401"): cic_type = CIC::CIC_NUS_8401; break;
|
|
case fnv_hash("cic5167"): cic_type = CIC::CIC_NUS_5167; break;
|
|
case fnv_hash("cicddus"): cic_type = CIC::CIC_NUS_DDUS; break;
|
|
case fnv_hash("cic5101"): cic_type = CIC::CIC_NUS_5101; break;
|
|
default: printf("Unknown tag: [%s] (skipping)\n", tag); break;
|
|
}
|
|
}
|
|
|
|
if (system_type == SystemType::UNKNOWN && cic_type != CIC::UNKNOWN) {
|
|
system_type = SystemType::NTSC;
|
|
}
|
|
|
|
printf("Region: %s, Save Type: %s, CIC: %s, CPak: %s, RPak: %s, TPak %s, RTC: %s, Mem: %uMiB\n",
|
|
stringify(system_type),
|
|
stringify(save_type),
|
|
stringify(cic_type),
|
|
stringify(cpak),
|
|
stringify(rpak),
|
|
stringify(tpak),
|
|
stringify(rtc),
|
|
(no_epak ? 4 : 8));
|
|
|
|
if (!is_auto()) {
|
|
printf("Auto-detect is OFF, not updating OSD settings.\n");
|
|
return true;
|
|
}
|
|
|
|
printf("Auto-detect is ON, updating OSD settings.\n");
|
|
|
|
if (system_type != SystemType::UNKNOWN) user_io_status_set(SYS_TYPE_OPT, (uint32_t)system_type);
|
|
if (cic_type != CIC::UNKNOWN) user_io_status_set(CIC_TYPE_OPT, (uint32_t)cic_type);
|
|
|
|
user_io_status_set(NO_EPAK_OPT, (uint32_t)no_epak);
|
|
user_io_status_set(CPAK_OPT, (uint32_t)cpak);
|
|
user_io_status_set(RPAK_OPT, (uint32_t)rpak);
|
|
user_io_status_set(TPAK_OPT, (uint32_t)tpak);
|
|
user_io_status_set(RTC_OPT, (uint32_t)rtc);
|
|
set_cart_save_type(save_type);
|
|
|
|
if (is_autopak() && ((PadType)user_io_status_get(CONTROLLER_OPTS[0]) != PadType::SNAC)) {
|
|
user_io_status_set(CONTROLLER_OPTS[0], (uint32_t)prefered_pad);
|
|
}
|
|
|
|
return (system_type != SystemType::UNKNOWN && cic_type != CIC::UNKNOWN);
|
|
}
|
|
|
|
static bool md5_matches(const char* line, const char* md5) {
|
|
for (size_t i = MD5_LENGTH * 2; i > 0; --i, line++, md5++) {
|
|
int c = *line;
|
|
if (!c || *md5 != tolower(c)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Returns numbers of matching characters if match, otherwize 0
|
|
static size_t cart_id_is_match(const char* line, const char* cart_id) {
|
|
const auto prefix_len = strlen(CARTID_PREFIX);
|
|
|
|
// A valid ID line should start with "ID:"
|
|
if (strncmp(line, CARTID_PREFIX, prefix_len)) {
|
|
return 0;
|
|
}
|
|
|
|
// Skip the line if it doesn't match our cart_id, '_' = don't care
|
|
const char* lp = line + prefix_len;
|
|
for (size_t i = 0; i < CARTID_LENGTH && *lp; i++, lp++) {
|
|
if (i && isspace(*lp)) {
|
|
return i; // Early termination
|
|
}
|
|
|
|
if (*lp != '_' && *lp != cart_id[i]) {
|
|
return 0; // Character didn't match pattern
|
|
}
|
|
}
|
|
|
|
return CARTID_LENGTH;
|
|
}
|
|
|
|
static uint8_t detect_rom_settings_in_db(const char* lookup_hash, const char* db_file_name) {
|
|
fileTextReader reader = {};
|
|
|
|
snprintf(full_path, sizeof(full_path), "%s/%s", HomeDir(), db_file_name);
|
|
|
|
if (!FileOpenTextReader(&reader, full_path)) {
|
|
printf("Failed to open N64 data file \"%s\".\n", db_file_name);
|
|
return 0;
|
|
}
|
|
|
|
while (const char* line = FileReadLine(&reader)) {
|
|
// Skip the line if it doesn't start with our hash
|
|
if (!md5_matches(line, lookup_hash)) continue;
|
|
|
|
const char* s = line + (MD5_LENGTH * 2);
|
|
char* tags = new char[strlen(s) + 1];
|
|
if (sscanf(s, "%*[ \t]%[^#;]", tags) <= 0) {
|
|
printf("Found ROM entry for MD5 %s, but the tag was malformed! (%s)\n", lookup_hash, s);
|
|
return 2;
|
|
}
|
|
|
|
printf("Found ROM entry for MD5 %s: [%s]\n", lookup_hash, tags);
|
|
|
|
// 2 = System region and/or CIC wasn't in DB, will need further detection
|
|
return parse_and_apply_db_tags(tags) ? 3 : 2;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static uint8_t detect_rom_settings_in_db_with_cartid(const char* cart_id, const char* db_file_name) {
|
|
fileTextReader reader = {};
|
|
|
|
snprintf(full_path, sizeof(full_path), "%s/%s", HomeDir(), db_file_name);
|
|
|
|
if (!FileOpenTextReader(&reader, full_path)) {
|
|
printf("Failed to open N64 data file \"%s\".\n", db_file_name);
|
|
return 0;
|
|
}
|
|
|
|
while (const char* line = FileReadLine(&reader)) {
|
|
// Skip lines that doesn't start with our ID
|
|
size_t i;
|
|
if (!(i = cart_id_is_match(line, cart_id))) continue;
|
|
|
|
auto s = line + strlen(CARTID_PREFIX) + i;
|
|
auto tags = new char[strlen(s) + 1];
|
|
if (sscanf(s, "%*[ \t]%[^#;]", tags) <= 0) {
|
|
printf("Found ROM entry for ID [%s], but the tag was malformed! \"%s\".\n", cart_id, s);
|
|
return 2;
|
|
}
|
|
|
|
printf("Found ROM entry for ID [%s]: \"%s\".\n", cart_id, tags);
|
|
|
|
// 2 = System region and/or CIC wasn't in DB, will need further detection
|
|
return parse_and_apply_db_tags(tags) ? 3 : 2;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const char* DB_FILE_NAMES[] = {
|
|
"N64-database_user.txt",
|
|
"N64-database.txt"
|
|
};
|
|
|
|
static uint8_t detect_rom_settings_in_dbs_with_md5(const char* lookup_hash) {
|
|
uint8_t detected = 0;
|
|
for (auto i = 0U; i < (sizeof(DB_FILE_NAMES) / sizeof(*DB_FILE_NAMES)); i++) {
|
|
if ((detected = detect_rom_settings_in_db(lookup_hash, DB_FILE_NAMES[i]))) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return detected;
|
|
}
|
|
|
|
static uint8_t detect_rom_settings_in_dbs_with_cartid(const char* lookup_id) {
|
|
// Check if all characters in the lookup are valid
|
|
for (size_t i = 0; i < CARTID_LENGTH; i++) {
|
|
auto c = lookup_id[i];
|
|
if (isalnum(c)) {
|
|
continue;
|
|
}
|
|
|
|
printf("Not a valid Cart ID: [%s]!\n", lookup_id);
|
|
return 0;
|
|
}
|
|
|
|
uint8_t detected = 0;
|
|
for (auto i = 0U; i < (sizeof(DB_FILE_NAMES) / sizeof(*DB_FILE_NAMES)); i++) {
|
|
if ((detected = detect_rom_settings_in_db_with_cartid(lookup_id, DB_FILE_NAMES[i]))) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return detected;
|
|
}
|
|
|
|
// "Advanced" Homebrew ROM Header https://n64brew.dev/wiki/ROM_Header
|
|
static bool detect_homebrew_header(const uint8_t* controller_settings, const char* cart_id) {
|
|
if ((cart_id[1] != 'E') || (cart_id[2] != 'D')) return false;
|
|
|
|
printf("Detected Advanced Homebrew ROM Header, how fancy!\n");
|
|
|
|
if (!is_auto()) {
|
|
printf("Auto-detect is OFF, not updating OSD settings.\n");
|
|
return false;
|
|
}
|
|
|
|
switch (hex_to_dec(cart_id[4])) {
|
|
case 1:
|
|
set_cart_save_type(MemoryType::EEPROM_512);
|
|
break;
|
|
case 2:
|
|
set_cart_save_type(MemoryType::EEPROM_2k);
|
|
break;
|
|
case 3:
|
|
set_cart_save_type(MemoryType::SRAM_32k);
|
|
break;
|
|
case 4:
|
|
set_cart_save_type(MemoryType::SRAM_96k);
|
|
break;
|
|
case 5:
|
|
set_cart_save_type(MemoryType::FLASH_128k);
|
|
break;
|
|
//case 6:
|
|
// set_cart_save_type(MemoryType::SRAM_128k);
|
|
// break;
|
|
default:
|
|
set_cart_save_type(MemoryType::NONE);
|
|
break;
|
|
}
|
|
|
|
printf("Auto-detect is ON, updating OSD settings.\n");
|
|
|
|
user_io_status_set(RTC_OPT, (uint32_t)(hex_to_dec(cart_id[5]) & 1)); // RTC
|
|
|
|
user_io_status_set(RPAK_OPT, (uint32_t)(
|
|
(controller_settings[0] == 0x01) ||
|
|
(controller_settings[1] == 0x01) ||
|
|
(controller_settings[2] == 0x01) ||
|
|
(controller_settings[3] == 0x01) ? 1 : 0)); // Rumble Pak
|
|
|
|
user_io_status_set(CPAK_OPT, (uint32_t)(
|
|
(controller_settings[0] == 0x02) ||
|
|
(controller_settings[1] == 0x02) ||
|
|
(controller_settings[2] == 0x02) ||
|
|
(controller_settings[3] == 0x02) ? 1 : 0)); // Controller Pak
|
|
|
|
user_io_status_set(TPAK_OPT, (uint32_t)(
|
|
(controller_settings[0] == 0x03) ||
|
|
(controller_settings[1] == 0x03) ||
|
|
(controller_settings[2] == 0x03) ||
|
|
(controller_settings[3] == 0x03) ? 1 : 0)); // Transfer Pak
|
|
|
|
if (!is_autopak()) return true;
|
|
|
|
size_t c_idx = 0;
|
|
for (auto c_opt : CONTROLLER_OPTS) {
|
|
if (controller_settings[c_idx] && ((PadType)user_io_status_get(c_opt) != PadType::SNAC)) {
|
|
if (controller_settings[c_idx] < 0x80) {
|
|
user_io_status_set(c_opt, (uint32_t)(
|
|
(controller_settings[c_idx] == 0x01) ? PadType::N64_PAD_WITH_RPAK :
|
|
(controller_settings[c_idx] == 0x02) ? PadType::N64_PAD_WITH_CPAK :
|
|
(controller_settings[c_idx] == 0x03) && (c_idx == 0) ? PadType::N64_PAD_WITH_TPAK :
|
|
PadType::N64_PAD));
|
|
}
|
|
else if (controller_settings[c_idx] == 0xff) {
|
|
user_io_status_set(c_opt, (uint32_t)PadType::UNPLUGGED);
|
|
}
|
|
}
|
|
c_idx++;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool detect_rom_settings_from_first_chunk(const char region_code, const uint64_t* signatures, size_t sig_len) {
|
|
SystemType system_type;
|
|
CIC cic = CIC::UNKNOWN;
|
|
bool is_known_signature = true;
|
|
|
|
switch (region_code) {
|
|
case 'D': // Germany
|
|
case 'F': // France
|
|
case 'H': // Netherlands
|
|
case 'I': // Italy
|
|
case 'L': // Gateway 64 (PAL)
|
|
case 'P': // Europe
|
|
case 'S': // Spain
|
|
case 'U': // Australia
|
|
case 'W': // Scandinavia
|
|
case 'X': // Europe
|
|
case 'Y': // Europe
|
|
case 'Z': // Europe
|
|
system_type = SystemType::PAL; break;
|
|
case 'A': // Asia (NTSC, used for 1080 US/JP)
|
|
case 'B': // Brazil
|
|
case 'C': // China
|
|
case 'E': // North America
|
|
case 'G': // Gateway 64 (NTSC)
|
|
case 'J': // Japan
|
|
case 'K': // Korea
|
|
case 'N': // Canada
|
|
default:
|
|
system_type = SystemType::NTSC; break;
|
|
}
|
|
|
|
if (sig_len) do {
|
|
switch (*signatures) {
|
|
default:
|
|
if (--sig_len) {
|
|
signatures++;
|
|
break;
|
|
}
|
|
printf("Unknown CIC signature: 0x%016llx, uses default.\n", *signatures);
|
|
is_known_signature = false;
|
|
// Fall through
|
|
case UINT64_C(0x000000a316adc55a): // CIC-6102/7101 IPL3
|
|
case UINT64_C(0x000000a30dacd530): // NOP:ed out CRC check
|
|
case UINT64_C(0x000000039c981107): // hcs64's CIC-6102 IPL3 replacement
|
|
case UINT64_C(0x000000d2828281b0): // Unknown. Used in some homebrew
|
|
case UINT64_C(0x000000d2531456f6): // Unknown. Used in some homebrew
|
|
case UINT64_C(0x000000d28bc5f6ae): // Unknown. Used in some homebrew
|
|
case UINT64_C(0x000000d2be3c4486): // Xeno Crisis custom IPL3
|
|
case UINT64_C(0x0000009acc31e644): // HW1 IPL3 (Turok E3 prototype)
|
|
case UINT64_C(0x0000009474732e6b): // IPL3 re-assembled with the GNU assembler (iQue)
|
|
cic = (system_type != SystemType::PAL) ? CIC::CIC_NUS_6102 : CIC::CIC_NUS_7101; break;
|
|
case UINT64_C(0x000000a405397b05): // CIC-7102 IPL3
|
|
case UINT64_C(0x000000a3fc388adb): // NOP:ed out CRC check
|
|
system_type = SystemType::PAL; cic = CIC::CIC_NUS_7102; break;
|
|
case UINT64_C(0x000000a0f26f62fe): // CIC-6101 IPL3
|
|
case UINT64_C(0x000000a0e96e72d4): // NOP:ed out CRC check
|
|
system_type = SystemType::NTSC; cic = CIC::CIC_NUS_6101; break;
|
|
case UINT64_C(0x000000a9229d7c45): // CIC-x103 IPL3
|
|
case UINT64_C(0x000000a9199c8c1b): // NOP:ed out CRC check
|
|
case UINT64_C(0x000000271316d406): // All zeros bar font (iQue Paper Mario)
|
|
cic = (system_type != SystemType::PAL) ? CIC::CIC_NUS_6103 : CIC::CIC_NUS_7103; break;
|
|
case UINT64_C(0x000000f8b860ed00): // CIC-x105 IPL3
|
|
case UINT64_C(0x000000f8af5ffcd6): // NOP:ed out CRC check
|
|
cic = (system_type != SystemType::PAL) ? CIC::CIC_NUS_6105 : CIC::CIC_NUS_7105; break;
|
|
case UINT64_C(0x000000ba5ba4b8cd): // CIC-x106 IPL3
|
|
cic = (system_type != SystemType::PAL) ? CIC::CIC_NUS_6106 : CIC::CIC_NUS_7106; break;
|
|
case UINT64_C(0x0000012daafc8aab): cic = CIC::CIC_NUS_5167; break;
|
|
case UINT64_C(0x000000a9df4b39e1): cic = CIC::CIC_NUS_8303; break;
|
|
case UINT64_C(0x000000aa764e39e1): cic = CIC::CIC_NUS_8401; break;
|
|
case UINT64_C(0x000000abb0b739e1): cic = CIC::CIC_NUS_DDUS; break;
|
|
case UINT64_C(0x00000081ce470326): // CIC-5101 IPL3
|
|
case UINT64_C(0x000000827a47195a): // Kuru Kuru Fever
|
|
case UINT64_C(0x00000082551e4848): // Tower & Shaft
|
|
cic = CIC::CIC_NUS_5101; break;
|
|
}
|
|
} while (cic == CIC::UNKNOWN);
|
|
|
|
printf("Region: %s, CIC: CIC-NUS-%s\n", stringify(system_type), stringify(cic));
|
|
|
|
if (!is_auto()) {
|
|
printf("Auto-detect is OFF, not updating OSD settings.\n");
|
|
return true;
|
|
}
|
|
|
|
printf("Auto-detect is ON, updating OSD settings.\n");
|
|
|
|
user_io_status_set(SYS_TYPE_OPT, (uint32_t)system_type);
|
|
user_io_status_set(CIC_TYPE_OPT, (uint32_t)cic);
|
|
|
|
return is_known_signature;
|
|
}
|
|
|
|
// Creates a lower-case hex string out of the MD5 buffer
|
|
static void md5_to_hex(uint8_t* md5, char* out) {
|
|
for (size_t i = 0; i < MD5_LENGTH; i++, md5++, out += 2)
|
|
sprintf(out, "%02x", *md5);
|
|
}
|
|
|
|
static void calc_bootcode_checksums(uint64_t bootcode_sums[2], const uint8_t* buf) {
|
|
size_t i;
|
|
uint64_t sum = 0;
|
|
|
|
// Calculate boot code checksum for bytes 0x40 - 0xc00 (Aleck64)
|
|
for (i = 0x40 / sizeof(uint32_t); i < 0xc00 / sizeof(uint32_t); i++) {
|
|
sum += ((uint32_t*)buf)[i];
|
|
}
|
|
bootcode_sums[1] = sum;
|
|
|
|
// Calculate boot code checksum for bytes 0x40 - 0x1000
|
|
for (; i < 0x1000 / sizeof(uint32_t); i++) {
|
|
sum += ((uint32_t*)buf)[i];
|
|
}
|
|
bootcode_sums[0] = sum;
|
|
}
|
|
|
|
static void create_save_path(char* save_path, const MemoryType type) {
|
|
create_path(SAVE_DIR, CoreName2);
|
|
sprintf(save_path, SAVE_DIR"/%s/", CoreName2);
|
|
char* fname = save_path + strlen(save_path);
|
|
|
|
char* p = strrchr(current_rom_path, '/');
|
|
if (p) {
|
|
strcat(fname, p + 1);
|
|
}
|
|
else {
|
|
strcat(fname, current_rom_path);
|
|
}
|
|
|
|
char ext[16];
|
|
|
|
switch (type) {
|
|
case MemoryType::EEPROM_512:
|
|
case MemoryType::EEPROM_2k:
|
|
strcpy(ext, ".eep");
|
|
break;
|
|
case MemoryType::SRAM_32k:
|
|
case MemoryType::SRAM_96k:
|
|
strcpy(ext, ".sra");
|
|
break;
|
|
case MemoryType::FLASH_128k:
|
|
strcpy(ext, ".fla");
|
|
break;
|
|
case MemoryType::CPAK:
|
|
case MemoryType::TPAK:
|
|
sprintf(ext, "_%u%s",
|
|
(mounted_save_files + ((get_cart_save_type() == MemoryType::NONE) ? 1 : 0)),
|
|
((type == MemoryType::CPAK) ? ".cpk" : ".tpk"));
|
|
break;
|
|
default:
|
|
strcpy(ext, ".sav");
|
|
break;
|
|
}
|
|
|
|
p = strrchr(fname, '.');
|
|
if (p) {
|
|
strcpy(p, ext);
|
|
}
|
|
else {
|
|
strcat(fname, ext);
|
|
}
|
|
}
|
|
|
|
static void mount_save_core(const char* save_path, const MemoryType type, const char* old_path) {
|
|
auto save_file = new N64SaveFile(type);
|
|
save_file->mount(save_path, old_path);
|
|
}
|
|
|
|
static void mount_save(const MemoryType type, const char* old_path) {
|
|
static char save_path[1024];
|
|
create_save_path(save_path, type);
|
|
mount_save_core(save_path, type, old_path);
|
|
}
|
|
|
|
static void unmount_all_saves() {
|
|
for (size_t i = 0; i < (sizeof(save_files) / sizeof(*save_files)); i++) {
|
|
if (save_files[i]) {
|
|
save_files[i]->unmount();
|
|
delete save_files[i];
|
|
save_files[i] = nullptr;
|
|
}
|
|
}
|
|
mounted_save_files = 0;
|
|
}
|
|
|
|
static void mount_save_gb(const char* old_path) {
|
|
static const size_t games_path_len = strlen(GAMES_DIR"/");
|
|
static const char* ext_gb_save = ".sav";
|
|
static const char* backup_suffix = "_bak";
|
|
static char save_path[1024];
|
|
static char tpak_path[1024];
|
|
static char backup_path[1024];
|
|
static char core_name[32] = "GAMEBOY";
|
|
|
|
create_save_path(tpak_path, MemoryType::TPAK);
|
|
|
|
char* p;
|
|
|
|
if (!*current_rom_path_gb) {
|
|
printf("No Game Boy game mounted. Will use regular .tpk save file.\n");
|
|
mount_save_core(tpak_path, MemoryType::TPAK, old_path);
|
|
return;
|
|
}
|
|
|
|
if (strncmp(current_rom_path_gb, GAMES_DIR"/", games_path_len) ||
|
|
!(p = strchr(current_rom_path_gb + games_path_len, '/'))) {
|
|
printf("Unrecognized Game Boy game save path. Will use \"/" SAVE_DIR"/%s/\".\n", core_name);
|
|
}
|
|
else {
|
|
const uint32_t core_name_len = p - (current_rom_path_gb + games_path_len);
|
|
if (!strncmp(current_rom_path_gb + games_path_len, CoreName2, core_name_len)) {
|
|
printf("Game Boy game loaded from \"/" GAMES_DIR"/%s/\". Will use \"/" SAVE_DIR"/%s/\".\n", CoreName2, core_name);
|
|
}
|
|
else {
|
|
memcpy(core_name, current_rom_path_gb + games_path_len, core_name_len);
|
|
core_name[core_name_len] = '\0';
|
|
printf("Game Boy core name is: %s\n", core_name);
|
|
}
|
|
}
|
|
|
|
sprintf(save_path, SAVE_DIR"/%s/", core_name);
|
|
char* fname = save_path + strlen(save_path);
|
|
|
|
p = strrchr(current_rom_path_gb, '/');
|
|
if (p) {
|
|
strcat(fname, p + 1);
|
|
}
|
|
else {
|
|
strcat(fname, current_rom_path_gb);
|
|
}
|
|
|
|
p = strrchr(fname, '.');
|
|
if (p) {
|
|
strcpy(p, ext_gb_save);
|
|
}
|
|
else {
|
|
strcat(fname, ext_gb_save);
|
|
}
|
|
|
|
auto save_file = new N64SaveFile(MemoryType::TPAK);
|
|
if (!FileExists(save_path)) {
|
|
create_path(SAVE_DIR, core_name);
|
|
if (!save_file->copy_file(tpak_path, save_path, true, true)) {
|
|
save_file->create_if_missing(save_path, old_path);
|
|
printf("Created new Game Boy save file \"/%s/\".\n", save_path);
|
|
}
|
|
else {
|
|
printf("Copied old .tpk save file to \"/%s/\".\n", save_path);
|
|
}
|
|
}
|
|
else {
|
|
save_file->copy_file(save_path, tpak_path, false, true);
|
|
strcpy(backup_path, save_path);
|
|
p = backup_path + strlen(save_path) - strlen(ext_gb_save);
|
|
strcpy(p, backup_suffix);
|
|
strcpy(p + strlen(backup_suffix), ext_gb_save);
|
|
if (!save_file->copy_file(save_path, backup_path, false)) {
|
|
printf("Failed to create a backup of \"/%s\"! Will use regular .tpk save file.\n", save_path);
|
|
save_file->mount(tpak_path, old_path);
|
|
return;
|
|
}
|
|
else {
|
|
printf("Created a backup of \"/%s\" at \"/%s\".\n", save_path, backup_path);
|
|
}
|
|
}
|
|
|
|
save_file->mount(save_path);
|
|
}
|
|
|
|
static void get_old_save_path(char* save_path) {
|
|
static const char* ext = ".sav";
|
|
|
|
create_path(SAVE_DIR, CoreName2);
|
|
sprintf(save_path, SAVE_DIR"/%s/", CoreName2);
|
|
char* fname = save_path + strlen(save_path);
|
|
|
|
char* p = strrchr(current_rom_path, '/');
|
|
if (p) {
|
|
strcat(fname, p + 1);
|
|
}
|
|
else {
|
|
strcat(fname, current_rom_path);
|
|
}
|
|
|
|
p = strrchr(fname, '.');
|
|
if (p) {
|
|
strcpy(p, ext);
|
|
}
|
|
else {
|
|
strcat(fname, ext);
|
|
}
|
|
}
|
|
|
|
void n64_load_savedata(uint64_t lba, int ack, uint64_t& buffer_lba, uint8_t* buffer, uint32_t buffer_size, uint32_t blksz, uint32_t sz) {
|
|
int invalid = 0;
|
|
int done = 0;
|
|
unsigned char file_idx = 0;
|
|
int64_t pos = lba * blksz;
|
|
N64SaveFile* save_file;
|
|
|
|
do {
|
|
if ((file_idx >= mounted_save_files) || !(save_file = save_files[file_idx]) || !save_file->is_mounted()) {
|
|
buffer_lba = -1;
|
|
invalid = 1;
|
|
break;
|
|
}
|
|
if (pos < get_save_offset(file_idx + 1)) {
|
|
break;
|
|
}
|
|
file_idx++;
|
|
} while (1);
|
|
|
|
fileTYPE* image;
|
|
if (!invalid && (image = save_file->get_image()) && image->size) {
|
|
diskled_on();
|
|
pos -= get_save_offset(file_idx);
|
|
uint32_t read_sz;
|
|
if (FileSeek(image, pos, SEEK_SET) && (read_sz = FileReadAdv(image, buffer, sz))) {
|
|
if ((save_file->type == MemoryType::CPAK) || (save_file->type == MemoryType::TPAK)) {
|
|
normalize_data(buffer, read_sz, ByteOrder::LITTLE_ENDIAN);
|
|
}
|
|
if (read_sz < sz) {
|
|
// Pad block that wasn't filled completely
|
|
memset(buffer + read_sz, 0, sz - read_sz);
|
|
}
|
|
done = 1;
|
|
buffer_lba = lba;
|
|
}
|
|
}
|
|
|
|
// Even after error we have to provide the block to the core
|
|
// Give an empty block.
|
|
if (!done || invalid) {
|
|
memset(buffer, 0, buffer_size);
|
|
}
|
|
|
|
// Data is now stored in buffer. Send it to fpga
|
|
EnableIO();
|
|
spi_w(UIO_SECTOR_RD | ack);
|
|
spi_block_write(buffer, user_io_get_width(), sz);
|
|
DisableIO();
|
|
|
|
if (done && ((pos + blksz) >= image->size)) {
|
|
printf("Loaded save data from \"%s\". (%lld bytes)\n", get_image_name(file_idx), image->size);
|
|
}
|
|
}
|
|
|
|
void n64_save_savedata(uint64_t lba, int ack, uint64_t& buffer_lba, uint8_t* buffer, uint32_t blksz, uint32_t sz) {
|
|
menu_process_save();
|
|
|
|
buffer_lba = -1;
|
|
int invalid = 0;
|
|
int done = 0;
|
|
unsigned char file_idx = 0;
|
|
int64_t pos = lba * blksz;
|
|
N64SaveFile* save_file;
|
|
|
|
do {
|
|
if ((file_idx >= mounted_save_files) || !(save_file = save_files[file_idx]) || !save_file->is_mounted()) {
|
|
invalid = 1;
|
|
break;
|
|
}
|
|
if (pos < get_save_offset(file_idx + 1)) {
|
|
break;
|
|
}
|
|
file_idx++;
|
|
} while (1);
|
|
|
|
// Fetch sector data from FPGA ...
|
|
EnableIO();
|
|
spi_w(UIO_SECTOR_WR | ack);
|
|
spi_block_read(buffer, user_io_get_width(), sz);
|
|
DisableIO();
|
|
|
|
if (invalid) {
|
|
return;
|
|
}
|
|
|
|
pos -= get_save_offset(file_idx);
|
|
fileTYPE* image;
|
|
|
|
if (!sz || !(image = save_file->get_image()) || (pos >= image->size)) {
|
|
return;
|
|
}
|
|
|
|
if (pos + sz > image->size) {
|
|
sz = image->size - pos;
|
|
}
|
|
|
|
diskled_on();
|
|
if (FileSeek(image, pos, SEEK_SET)) {
|
|
if ((save_file->type == MemoryType::CPAK) || (save_file->type == MemoryType::TPAK)) {
|
|
normalize_data(buffer, sz, ByteOrder::LITTLE_ENDIAN);
|
|
}
|
|
done = FileWriteAdv(image, buffer, sz, -1) >= 0;
|
|
}
|
|
|
|
if (done && ((pos + blksz) >= image->size)) {
|
|
printf("Saved save data to \"%s\". (%lld bytes)\n", get_image_name(file_idx), image->size);
|
|
}
|
|
}
|
|
|
|
static void mount_all_saves() {
|
|
get_old_save_path(old_save_path);
|
|
auto cart_save_type = get_cart_save_type();
|
|
|
|
if (cart_save_type != MemoryType::NONE) {
|
|
mount_save(cart_save_type, old_save_path);
|
|
}
|
|
|
|
auto use_cpak = (bool)user_io_status_get(CPAK_OPT);
|
|
|
|
// First controller can be either tpak or cpak. Tpak is prioritized.
|
|
if ((bool)user_io_status_get(TPAK_OPT)) {
|
|
mount_save_gb(old_save_path);
|
|
}
|
|
else if (use_cpak) {
|
|
mount_save(MemoryType::CPAK, old_save_path);
|
|
}
|
|
|
|
if (use_cpak) {
|
|
mount_save(MemoryType::CPAK, old_save_path);
|
|
mount_save(MemoryType::CPAK, old_save_path);
|
|
mount_save(MemoryType::CPAK, old_save_path);
|
|
}
|
|
}
|
|
|
|
static int cheat_compare(const volatile uint8_t* mem, const int32_t new_val, const cheat_code_flags flags) {
|
|
if (flags.cmp_type == ComparisionType::OPTYPE_ALWAYS)
|
|
return 1;
|
|
|
|
uint32_t old_val_masked = 0, new_val_masked = 0;
|
|
|
|
// Mask and reverse
|
|
for (size_t i = 0; flags.cmp_mask >> i; i++) {
|
|
old_val_masked <<= 8;
|
|
new_val_masked <<= 8;
|
|
if (flags.cmp_mask & (0x1 << i)) {
|
|
old_val_masked |= mem[i];
|
|
new_val_masked |= (new_val >> (i * 8)) & 0xff;
|
|
}
|
|
}
|
|
|
|
// Return "0" if not equal according to comparision type, so next code will get skipped
|
|
switch (flags.cmp_type) {
|
|
case ComparisionType::OPTYPE_EQUALS:
|
|
return new_val_masked == old_val_masked;
|
|
case ComparisionType::OPTYPE_GREATER:
|
|
return new_val_masked > old_val_masked;
|
|
case ComparisionType::OPTYPE_LESS:
|
|
return new_val_masked < old_val_masked;
|
|
case ComparisionType::OPTYPE_GREATER_EQ:
|
|
return new_val_masked >= old_val_masked;
|
|
case ComparisionType::OPTYPE_LESS_EQ:
|
|
return new_val_masked <= old_val_masked;
|
|
case ComparisionType::OPTYPE_NOT_EQ:
|
|
return new_val_masked != old_val_masked;
|
|
default:
|
|
// Unknown comparision type
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
static int cheat_execute(cheat_code* code) {
|
|
if ((code->address >= RAM_SIZE) || !code->flags.cmp_mask)
|
|
return -1;
|
|
|
|
// Boot code, run only once. Skip if already executed.
|
|
if (code->flags.is_boot_code) {
|
|
if (code->flags.is_boot_code_executed)
|
|
return 1;
|
|
|
|
code->flags.is_boot_code_executed = true;
|
|
}
|
|
|
|
// Game Shark button code. Only run if certain button is pressed. Hard-coded to F5 right now.
|
|
if (code->flags.is_gs_button_code && !is_key_pressed(63))
|
|
return 1;
|
|
|
|
volatile uint8_t* mem = ((volatile uint8_t*)rdram_ptr) + code->address;
|
|
|
|
int result = cheat_compare(mem, code->replace, code->flags);
|
|
if (result != 1) return result;
|
|
|
|
if ((code->flags.cmp_mask == 0xf) && !(code->address & 0x3)) {
|
|
// 32-bit aligned write
|
|
*(uint32_t*)mem = code->replace;
|
|
}
|
|
else if ((code->flags.cmp_mask == 0x3) && !(code->address & 0x1)) {
|
|
// 16-bit aligned write
|
|
*(uint16_t*)mem = code->replace & 0xffff;
|
|
}
|
|
else {
|
|
// Write byte-by-byte to memory in big-endian order
|
|
if (code->flags.cmp_mask & (0x1 << 0)) mem[0] = (code->replace >> (8 * 0)) & 0xff;
|
|
if (code->flags.cmp_mask & (0x1 << 1)) mem[1] = (code->replace >> (8 * 1)) & 0xff;
|
|
if (code->flags.cmp_mask & (0x1 << 2)) mem[2] = (code->replace >> (8 * 2)) & 0xff;
|
|
if (code->flags.cmp_mask & (0x1 << 3)) mem[3] = (code->replace >> (8 * 3)) & 0xff;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
void n64_cheats_send(const void* buf_ptr, const uint32_t size) {
|
|
cheat_codes = (cheat_code*)buf_ptr;
|
|
cheat_codes_count = size;
|
|
}
|
|
|
|
static unsigned long poll_timer = 0;
|
|
|
|
void n64_reset() {
|
|
printf("Resetting N64...\n");
|
|
if (cheat_codes && cheats_loaded() && cheats_enabled()) {
|
|
for (uint32_t i = 0; i < cheat_codes_count; i++) {
|
|
cheat_codes[i].flags.is_boot_code_executed = false;
|
|
}
|
|
|
|
poll_timer = GetTimer(2500);
|
|
}
|
|
}
|
|
|
|
void n64_poll() {
|
|
static uint8_t adj = 0;
|
|
|
|
if (!poll_timer || CheckTimer(poll_timer)) {
|
|
|
|
if (!(loaded && is_fpga_ready(0))) {
|
|
poll_timer = GetTimer(1000);
|
|
printf("Waiting for N64 game to be loaded...\n");
|
|
return;
|
|
}
|
|
|
|
auto system_type = (SystemType)user_io_status_get(SYS_TYPE_OPT);
|
|
|
|
// 17, 17, 16, 17, 17, 16... Repeated, to achieve (1 / .01667 ms) = 60 Hz.
|
|
poll_timer = GetTimer((system_type == SystemType::PAL) ? 20 : ((adj > 1) ? 17 : 16));
|
|
if (adj > 3 || --adj == 0) adj = 3;
|
|
|
|
if (cheats_loaded()) {
|
|
if (rdram_ptr == (void*)-1) return;
|
|
|
|
if (!rdram_ptr) {
|
|
if (!(rdram_ptr = shmem_map(0x30000000, RAM_SIZE))) {
|
|
rdram_ptr = (void*)-1;
|
|
printf("Failed to map RDRAM!\n");
|
|
Info("Failed to initialize cheat engine!", 2000);
|
|
}
|
|
else {
|
|
printf("Mapped RDRAM at 0x%" PRIXPTR ".\n", (uintptr_t)rdram_ptr);
|
|
}
|
|
}
|
|
else if (cheat_codes && cheats_enabled()) {
|
|
for (uint32_t i = 0; i < cheat_codes_count; i++) {
|
|
switch (cheat_execute(&cheat_codes[i])) {
|
|
case 0:
|
|
// Unfullfilled conditional code (DXXXXXXX), skip the next flagged code(s)
|
|
while ((i < (cheat_codes_count - 1)) && (cheat_codes[i + 1].compare & 0x1)) {
|
|
i++;
|
|
}
|
|
// Fall through
|
|
case 1:
|
|
// Normal code (8XXXXXXX)
|
|
continue;
|
|
}
|
|
|
|
// Invalid or unhandled code.
|
|
printf("Invalid cheat code: %08x\t%08x\t%08x\t%08x !\n",
|
|
cheat_codes[i].address,
|
|
cheat_codes[i].compare,
|
|
cheat_codes[i].replace,
|
|
*(uint32_t*)&cheat_codes[i].flags);
|
|
Info("Invalid cheat code! Disabling cheats.", 1500);
|
|
cheats_disable();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
int n64_rom_tx(const char* name, unsigned char idx, uint32_t load_addr, uint32_t& file_crc) {
|
|
static uint8_t buf[4096];
|
|
fileTYPE f;
|
|
|
|
if (!FileOpen(&f, name, 1)) {
|
|
return 0;
|
|
}
|
|
|
|
uint32_t data_size = f.size;
|
|
uint32_t data_left = data_size;
|
|
|
|
printf("N64 file \"%s\" with %u bytes to send for index %02x.\n", name, data_size, idx);
|
|
|
|
unmount_all_saves();
|
|
|
|
// Set index byte
|
|
user_io_set_index(idx);
|
|
|
|
if (((idx & 0x3f) == 2) && data_size) {
|
|
// Handle non-N64 files (Game Boy)
|
|
|
|
bool should_reset = false;
|
|
|
|
if (!(*current_rom_path_gb) || strcmp(current_rom_path_gb, name)) {
|
|
strcpy(current_rom_path_gb, name);
|
|
if (*current_rom_path && user_io_status_get(TPAK_OPT)) {
|
|
mount_all_saves();
|
|
|
|
// Force reload of backup RAM
|
|
user_io_status_set(RELOAD_SAVE_OPT, 1);
|
|
usleep(100000);
|
|
user_io_status_set(RELOAD_SAVE_OPT, 0);
|
|
|
|
// Hold reset
|
|
user_io_status_set(RESET_OPT, 1);
|
|
|
|
should_reset = true;
|
|
}
|
|
}
|
|
|
|
// Prepare transmission of Game Boy file
|
|
user_io_set_download(1);
|
|
ProgressMessage();
|
|
|
|
while (data_left) {
|
|
uint32_t chunk = (data_left > sizeof(buf)) ? sizeof(buf) : data_left;
|
|
FileReadAdv(&f, buf, chunk);
|
|
|
|
user_io_file_tx_data(buf, chunk);
|
|
|
|
ProgressMessage("Loading", f.name, data_size - data_left, data_size);
|
|
data_left -= chunk;
|
|
}
|
|
|
|
printf("Done loading Game Boy ROM.\n");
|
|
FileClose(&f);
|
|
|
|
// Signal end of transmission
|
|
user_io_set_download(0);
|
|
ProgressMessage();
|
|
|
|
if (should_reset) {
|
|
// Release reset
|
|
user_io_status_set(RESET_OPT, 0);
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
loaded = 0;
|
|
|
|
if (rdram_ptr && rdram_ptr != (void*)-1) {
|
|
shmem_unmap(rdram_ptr, RAM_SIZE);
|
|
rdram_ptr = nullptr;
|
|
}
|
|
|
|
// save state processing
|
|
process_ss(name);
|
|
|
|
/* 0 = Nothing detected
|
|
1 = System region and CIC detected
|
|
2 = Found some ROM info in DB (Save type etc.), but System region and/or CIC has not been determined
|
|
3 = Has detected everything, System type, CIC, Save type etc. */
|
|
uint8_t rom_settings_detected = 0;
|
|
ByteOrder rom_endianness;
|
|
uint8_t md5[MD5_LENGTH];
|
|
char md5_hex[MD5_LENGTH * 2 + 1];
|
|
uint64_t bootcode_sums[2] = { };
|
|
uint8_t controller_settings[4] = { };
|
|
bool is_first_chunk = true;
|
|
char cart_id[CARTID_LENGTH + 1] = { };
|
|
char internal_name[20 + 1];
|
|
|
|
memset(patches, 0, sizeof(patches));
|
|
|
|
// CRC32 is used for cheat look-up
|
|
file_crc = 0;
|
|
|
|
void* mem = load_addr ? (uint8_t*)shmem_map(fpga_mem(load_addr), data_size) : nullptr;
|
|
uint8_t* write_ptr = (uint8_t*)mem;
|
|
|
|
MD5Context ctx;
|
|
MD5Init(&ctx);
|
|
|
|
// prepare transmission of new file
|
|
user_io_set_download(1, load_addr ? data_size : 0);
|
|
ProgressMessage();
|
|
|
|
while (data_left) {
|
|
size_t chunk = (data_left > sizeof(buf)) ? sizeof(buf) : data_left;
|
|
|
|
FileReadAdv(&f, buf, chunk);
|
|
|
|
// Perform sanity checks and detect ROM endianness
|
|
if (is_first_chunk) {
|
|
if (chunk < 4096) {
|
|
// Signal end of transmission
|
|
user_io_set_download(0);
|
|
*current_rom_path = '\0';
|
|
printf("Failed to load ROM: must be at least 4096 bytes.\n");
|
|
|
|
return 0;
|
|
}
|
|
|
|
rom_endianness = detect_rom_endianness(buf);
|
|
}
|
|
|
|
// Normalize data to big-endian format, if needed
|
|
normalize_data(buf, chunk, rom_endianness);
|
|
MD5Update(&ctx, buf, chunk);
|
|
|
|
if (is_first_chunk) {
|
|
/* Try to detect ROM settings based on header MD5 hash.
|
|
For calculating the MD5 hash of the header, we need to make a
|
|
copy of the context before calling MD5Final, otherwise the file
|
|
hash will be incorrect later on. */
|
|
|
|
MD5Context ctx_header;
|
|
memcpy(&ctx_header, &ctx, sizeof(struct MD5Context));
|
|
MD5Final(md5, &ctx_header);
|
|
md5_to_hex(md5, md5_hex);
|
|
printf("Header MD5 hash: %s\n", md5_hex);
|
|
|
|
trim(internal_name, 20, (char*)&buf[0x20]);
|
|
rom_settings_detected = detect_rom_settings_in_dbs_with_md5(md5_hex);
|
|
memcpy(controller_settings, &buf[0x34], sizeof(controller_settings));
|
|
calc_bootcode_checksums(bootcode_sums, buf);
|
|
|
|
/* The first byte (starting at 0x3b) indicates the type of ROM
|
|
'N' = Cartridge
|
|
'D' = 64DD disk
|
|
'C' = Cartridge part of expandable game
|
|
'E' = 64DD expansion for cart
|
|
'Z' = Aleck64 cart
|
|
The 2nd and 3rd byte form a 2-letter ID for the game
|
|
The 4th byte indicates the region and language for the game
|
|
The 5th byte indicates the revision of the game */
|
|
|
|
auto p_cid = (char*)&buf[0x3b];
|
|
for (auto i = 0; i < 4; i++, p_cid++) {
|
|
if (isalnum(*p_cid)) {
|
|
cart_id[i] = *p_cid;
|
|
}
|
|
else {
|
|
cart_id[i] = '?';
|
|
}
|
|
}
|
|
|
|
if (strncmp(cart_id, "????", 4)) {
|
|
sprintf(cart_id + 4, "%02X", buf[0x3f]);
|
|
printf("Cartridge ID: %s\n", cart_id);
|
|
}
|
|
else {
|
|
memset(cart_id, '\0', CARTID_LENGTH);
|
|
}
|
|
}
|
|
|
|
// Copy to DDR memory for fast ROM loading
|
|
if (mem) {
|
|
memcpy(write_ptr, buf, chunk);
|
|
write_ptr += chunk;
|
|
}
|
|
else {
|
|
// Fallback to normal (slow) loading
|
|
user_io_file_tx_data(buf, chunk);
|
|
}
|
|
|
|
ProgressMessage("Loading", f.name, data_size - data_left, data_size);
|
|
data_left -= chunk;
|
|
is_first_chunk = false;
|
|
|
|
// CRC32 is used for cheat look-up. Cheat files from gamehacking.org use byte swapped CRC32 for some reason...
|
|
normalize_data(buf, chunk, ByteOrder::BYTE_SWAPPED);
|
|
file_crc = crc32(file_crc, buf, chunk);
|
|
}
|
|
|
|
MD5Final(md5, &ctx);
|
|
md5_to_hex(md5, md5_hex);
|
|
printf("File MD5: %s\n", md5_hex);
|
|
|
|
// Try to detect ROM settings from full file MD5 if they're are not detected yet
|
|
if (!rom_settings_detected) {
|
|
printf("No ROM information found for header hash.\n");
|
|
rom_settings_detected = detect_rom_settings_in_dbs_with_md5(md5_hex);
|
|
// Try to detect ROM settings by cart ID if they're are not detected yet
|
|
if (!rom_settings_detected) {
|
|
printf("No ROM information found for file hash.\n");
|
|
rom_settings_detected = detect_rom_settings_in_dbs_with_cartid(cart_id);
|
|
if (!rom_settings_detected) {
|
|
if (detect_homebrew_header(controller_settings, cart_id)) {
|
|
rom_settings_detected = 2;
|
|
}
|
|
else {
|
|
printf("No ROM information found for Cart ID.\n");
|
|
if (is_auto()) {
|
|
// Defaulting misc. System Settings, everything OFF
|
|
user_io_status_set(NO_EPAK_OPT, 0); // Enable Expansion Pak
|
|
user_io_status_set(CPAK_OPT, 0); // Disable Controller Pak
|
|
user_io_status_set(RPAK_OPT, 0); // Disable Rumble Pak
|
|
user_io_status_set(TPAK_OPT, 0); // Disable Transfer Pak
|
|
user_io_status_set(RTC_OPT, 0); // Disable RTC
|
|
set_cart_save_type(MemoryType::NONE); // Disable Save
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!(rom_settings_detected & 1) &&
|
|
detect_rom_settings_from_first_chunk(cart_id[3], bootcode_sums, sizeof(bootcode_sums) / sizeof(*bootcode_sums))) {
|
|
// Try detect (partial) ROM settings by analyzing the ROM itself. (System region and CIC)
|
|
rom_settings_detected |= 1;
|
|
}
|
|
|
|
printf("Done loading N64 ROM.\n");
|
|
FileClose(&f);
|
|
|
|
bool is_patched = false;
|
|
|
|
if (mem) {
|
|
if (patches_enabled()) {
|
|
for (auto patch = patches; patch->address * 4 < data_size && patch->diff; patch++) {
|
|
if (patch->turbo_core_only && !is_turbo_core()) continue;
|
|
((uint32_t*)mem)[patch->address] ^= patch->diff;
|
|
is_patched = true;
|
|
}
|
|
}
|
|
|
|
shmem_unmap(mem, data_size);
|
|
}
|
|
|
|
strcpy(current_rom_path, name);
|
|
|
|
mount_all_saves();
|
|
|
|
// Signal end of transmission
|
|
user_io_set_download(0);
|
|
ProgressMessage();
|
|
|
|
loaded = 1;
|
|
|
|
if (!cfg.controller_info || !is_auto()) {
|
|
return 1;
|
|
}
|
|
|
|
auto system_type = SystemType::UNKNOWN;
|
|
auto cic = CIC::UNKNOWN;
|
|
|
|
char info[256];
|
|
size_t len = sprintf(info, "Auto-detect:");
|
|
if (*cart_id && ((cart_id[1] != 'E') || (cart_id[2] != 'D'))) len += sprintf(info + len, "\n[%.4s] v.%u.%u", cart_id, hex_to_dec(cart_id[4]) + 1, hex_to_dec(cart_id[5]));
|
|
if (*internal_name) len += sprintf(info + len, "\n\"%s\"", internal_name);
|
|
if (!(rom_settings_detected & 1)) {
|
|
len += sprintf(info + len, "\nUnknown Region/CIC");
|
|
}
|
|
else {
|
|
system_type = (SystemType)user_io_status_get(SYS_TYPE_OPT);
|
|
cic = (CIC)user_io_status_get(CIC_TYPE_OPT);
|
|
len += sprintf(info + len, "\nRegion: %s (%s)", stringify(system_type), stringify(cic));
|
|
}
|
|
|
|
if (!(rom_settings_detected & 2)) {
|
|
sprintf(info + len, "\nROM missing from database.\nYou might not be able to save.");
|
|
|
|
Info(info, cfg.controller_info * 1000);
|
|
}
|
|
else {
|
|
auto save_type = get_cart_save_type();
|
|
auto no_epak = (bool)user_io_status_get(NO_EPAK_OPT);
|
|
auto tpak = (bool)user_io_status_get(TPAK_OPT);
|
|
auto cpak = (bool)user_io_status_get(CPAK_OPT);
|
|
auto rpak = (bool)user_io_status_get(RPAK_OPT);
|
|
auto rtc = (bool)user_io_status_get(RTC_OPT);
|
|
|
|
if (save_type != MemoryType::NONE) len += sprintf(info + len, "\nSave Type: %s", stringify(save_type));
|
|
if (tpak) len += sprintf(info + len, "\nTransfer Pak \x96");
|
|
if (cpak) len += sprintf(info + len, "\nController Pak \x96");
|
|
if (rpak) len += sprintf(info + len, "\nRumble Pak \x96");
|
|
if (rtc) len += sprintf(info + len, "\nRTC \x96");
|
|
if (no_epak) len += sprintf(info + len, "\nDisable Exp. Pak \x96");
|
|
if (is_patched) sprintf(info + len, "\nPatched \x96");
|
|
|
|
Info(info, cfg.controller_info * 1000);
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
#pragma pop_macro("NONE")
|
|
#pragma pop_macro("BIG_ENDIAN")
|
|
#pragma pop_macro("LITTLE_ENDIAN")
|