mirror of
https://github.com/MiSTer-devel/Main_MiSTer.git
synced 2026-04-19 03:04:28 +00:00
Got rid of the annoying message you get when opening the menu in a game that uses cpak. It said "Saving..." even when there wasn't anything to save. Cleaned up and refactored the save file code. Much easier to follow now.
1806 lines
51 KiB
C++
1806 lines
51 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;
|
|
}
|
|
|
|
bool needs_byteswap() const {
|
|
return (this->type == MemoryType::CPAK || this->type == MemoryType::TPAK);
|
|
}
|
|
|
|
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;
|
|
// Normalize data to big-endian format, if needed
|
|
if (this->needs_byteswap()) {
|
|
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);
|
|
}
|
|
}
|
|
|
|
// Find the correct save file and offset for a given LBA
|
|
static N64SaveFile* resolve_save_file(const uint64_t sector_index, const uint32_t sector_size, const uint32_t chunk_size,
|
|
uint8_t& out_file_index, uint64_t& out_file_offset) {
|
|
out_file_index = 0;
|
|
out_file_offset = 0;
|
|
|
|
if (!chunk_size) return nullptr;
|
|
|
|
int64_t absolute_pos = sector_index * sector_size;
|
|
|
|
for (; out_file_index < mounted_save_files; ++out_file_index) {
|
|
// This file ends where the next one begins.
|
|
uint32_t file_end = get_save_offset(out_file_index + 1);
|
|
|
|
// If our position is before this boundary, it belongs to this file slot.
|
|
if (absolute_pos < file_end) {
|
|
N64SaveFile* file = save_files[out_file_index];
|
|
if (!file || !file->is_mounted()) return nullptr;
|
|
|
|
// Calculate offset relative to the start of this specific file.
|
|
out_file_offset = absolute_pos - get_save_offset(out_file_index);
|
|
return file;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
static void write_save_file(const uint64_t sector_index, const uint32_t sector_size, const int ack_flags,
|
|
uint8_t* buffer, uint32_t chunk_size) {
|
|
uint64_t file_offset;
|
|
uint8_t file_index;
|
|
|
|
// Fetch sector data from FPGA.
|
|
EnableIO();
|
|
spi_w(UIO_SECTOR_WR | ack_flags);
|
|
spi_block_read(buffer, user_io_get_width(), chunk_size);
|
|
DisableIO();
|
|
|
|
N64SaveFile* file = resolve_save_file(sector_index, sector_size, chunk_size, file_index, file_offset);
|
|
if (!file) return;
|
|
|
|
fileTYPE* img = file->get_image();
|
|
if (!img || ((uint64_t)img->size < file_offset)) return;
|
|
|
|
// Clamp write size to file bounds.
|
|
if ((file_offset + chunk_size) > (uint64_t)img->size) {
|
|
chunk_size = img->size - file_offset;
|
|
}
|
|
|
|
if (FileSeek(img, file_offset, SEEK_SET)) {
|
|
diskled_on();
|
|
|
|
if (file->needs_byteswap()) {
|
|
normalize_data(buffer, chunk_size, ByteOrder::LITTLE_ENDIAN);
|
|
}
|
|
|
|
// Only write save data if it's different from the old one.
|
|
uint8_t existing_data[chunk_size];
|
|
if ((chunk_size != (uint32_t)FileReadAdv(img, existing_data, chunk_size)) || memcmp(buffer, existing_data, chunk_size)) {
|
|
menu_process_save();
|
|
FileSeek(img, file_offset, SEEK_SET);
|
|
FileWriteAdv(img, buffer, chunk_size);
|
|
}
|
|
|
|
// Log success if we hit the end of the file.
|
|
if ((file_offset + sector_size) >= (uint64_t)img->size) {
|
|
printf("Wrote N64 save data to \"%s\". (%lld bytes)\n", get_image_name(file_index), img->size);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void read_save_file(const uint64_t sector_index, const uint32_t sector_size, const int ack_flags,
|
|
uint64_t& confirmed_sector, uint8_t* buffer, const uint32_t buffer_capacity, uint32_t chunk_size) {
|
|
uint8_t file_index;
|
|
uint64_t file_offset;
|
|
|
|
N64SaveFile* file = resolve_save_file(sector_index, sector_size, chunk_size, file_index, file_offset);
|
|
|
|
if (file) {
|
|
fileTYPE* img = file->get_image();
|
|
if (img && img->size) {
|
|
diskled_on();
|
|
|
|
if (FileSeek(img, file_offset, SEEK_SET)) {
|
|
const uint32_t bytes_read = (uint32_t)FileReadAdv(img, buffer, chunk_size);
|
|
|
|
if (file->needs_byteswap()) {
|
|
normalize_data(buffer, bytes_read, ByteOrder::LITTLE_ENDIAN);
|
|
}
|
|
|
|
// Pad incomplete chunks with zeros.
|
|
if (bytes_read < chunk_size) {
|
|
memset(buffer + bytes_read, 0, chunk_size - bytes_read);
|
|
}
|
|
|
|
// Log success if we hit the end of the file.
|
|
if ((file_offset + sector_size) >= (uint64_t)img->size) {
|
|
printf("Read N64 save data from \"%s\". (%lld bytes)\n", get_image_name(file_index), img->size);
|
|
}
|
|
|
|
confirmed_sector = sector_index;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle failure case (file not found, unmounted, or read error).
|
|
if (confirmed_sector == -1LLU) {
|
|
printf("Couldn't Read N64 save data from \"%s\"!!\n", get_image_name(file_index));
|
|
memset(buffer, 0, buffer_capacity);
|
|
}
|
|
|
|
// Send sector data to FPGA.
|
|
EnableIO();
|
|
spi_w(UIO_SECTOR_RD | ack_flags);
|
|
spi_block_write(buffer, user_io_get_width(), chunk_size);
|
|
DisableIO();
|
|
}
|
|
|
|
/* The N64 core handles all its save types by sending all of them concatenated together to/from the FPGA.
|
|
e.g. (EEPROM/SRAM/FLASH) (+ TPAK) (+ CPAK 1) (+ CPAK 2) (+ CPAK 3) (+ CPAK 4)
|
|
We need to split up the continuous data stream into files if we want to save.
|
|
We need to merge the files into a single continuous data stream if we want to load. */
|
|
bool n64_process_save(const bool use_save, const int op, const uint64_t sector_index, const uint32_t sector_size, const int ack_flags,
|
|
uint64_t& confirmed_sector, uint8_t* buffer, const uint32_t buffer_capacity, uint32_t chunk_size) {
|
|
if (!use_save || !(op & 3)) return false;
|
|
|
|
confirmed_sector = -1LLU; // Default to error state.
|
|
|
|
if (op == 2) {
|
|
write_save_file(sector_index, sector_size, ack_flags, buffer, chunk_size);
|
|
}
|
|
else {
|
|
read_save_file(sector_index, sector_size, ack_flags, confirmed_sector, buffer, buffer_capacity, chunk_size);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
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, const unsigned char idx, const 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");
|
|
printf("CRC32: %08X\n", file_crc);
|
|
FileClose(&f);
|
|
|
|
// Write game ID if cart_id exists (BIOS check is handled in user_io_write_gameid)
|
|
if (cart_id[0])
|
|
{
|
|
user_io_write_gameid(name, file_crc, cart_id);
|
|
}
|
|
|
|
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")
|