mirror of
https://github.com/GerbilSoft/rom-properties.git
synced 2025-06-18 11:35:38 -04:00
[libromdata] SNES: List the Nintendo Power directory for full NP cartridge dumps.
Currently shows the directory index, title (in Japanese), game code, timestamp (directly as it's listed, no timezone adjustments), and kiosk ID.
This commit is contained in:
parent
02d7fbfdfa
commit
edd89a0b1c
1
NEWS.md
1
NEWS.md
@ -6,6 +6,7 @@
|
||||
* ISO: Show the sector mode in addition to sector sizes.
|
||||
* Fixes #322: Underlying CD image type for ISO
|
||||
* Reported by @DankRank.
|
||||
* SNES: List the Nintendo Power directory for full NP cartridge dumps.
|
||||
|
||||
* Bug fixes:
|
||||
* NintendoDS_BNR: Animated icons were missing the animated icon flag.
|
||||
|
@ -21,6 +21,7 @@ using namespace LibRpFile;
|
||||
// C++ STL classes
|
||||
using std::array;
|
||||
using std::string;
|
||||
using std::unique_ptr;
|
||||
using std::vector;
|
||||
|
||||
namespace LibRomData {
|
||||
@ -113,6 +114,13 @@ public:
|
||||
* @return Game ID if available; empty string if not.
|
||||
*/
|
||||
string getGameID(bool doFake = false) const;
|
||||
|
||||
public:
|
||||
/**
|
||||
* Add Nintendo Power fields.
|
||||
* @return 0 on success; negative POSIX error code on error.
|
||||
*/
|
||||
int addFields_NP(void);
|
||||
};
|
||||
|
||||
ROMDATA_IMPL(SNES)
|
||||
@ -365,13 +373,13 @@ bool SNESPrivate::isSnesRomHeaderValid(const SNES_RomHeader *romHeader, bool isH
|
||||
if (romHeader->snes.old_publisher_code == 0x33) {
|
||||
// Extended header should be present.
|
||||
// New publisher code and game ID must be alphanumeric.
|
||||
if (!ISALNUM(romHeader->snes.ext.new_publisher_code[0]) ||
|
||||
!ISALNUM(romHeader->snes.ext.new_publisher_code[1]))
|
||||
if (!ISALNUM(romHeader->snes.ext.new_publisher_code.c[0]) ||
|
||||
!ISALNUM(romHeader->snes.ext.new_publisher_code.c[1]))
|
||||
{
|
||||
// New publisher code is invalid.
|
||||
// NOTE: Allowing '00' for certain prototypes or homebrew.
|
||||
if (romHeader->snes.ext.new_publisher_code[0] != 0 ||
|
||||
romHeader->snes.ext.new_publisher_code[1] != 0)
|
||||
if (romHeader->snes.ext.new_publisher_code.c[0] != 0 ||
|
||||
romHeader->snes.ext.new_publisher_code.c[1] != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@ -495,8 +503,8 @@ bool SNESPrivate::isBsxRomHeaderValid(const SNES_RomHeader *romHeader, bool isHi
|
||||
// FIXME: Some BS-X ROMs have an invalid publisher code...
|
||||
#if 0
|
||||
// New publisher code must be alphanumeric.
|
||||
if (!ISALNUM(romHeader->bsx.ext.new_publisher_code[0]) ||
|
||||
!ISALNUM(romHeader->bsx.ext.new_publisher_code[1]))
|
||||
if (!ISALNUM(romHeader->bsx.ext.new_publisher_code.c[0]) ||
|
||||
!ISALNUM(romHeader->bsx.ext.new_publisher_code.c[1]))
|
||||
{
|
||||
// New publisher code is invalid.
|
||||
return false;
|
||||
@ -610,23 +618,23 @@ string SNESPrivate::getPublisher(void) const
|
||||
// Publisher.
|
||||
if (romHeader.snes.old_publisher_code == 0x33) {
|
||||
// New publisher code.
|
||||
publisher = NintendoPublishers::lookup(romHeader.snes.ext.new_publisher_code);
|
||||
publisher = NintendoPublishers::lookup(romHeader.snes.ext.new_publisher_code.c);
|
||||
if (publisher) {
|
||||
s_publisher = publisher;
|
||||
} else {
|
||||
if (ISALNUM(romHeader.snes.ext.new_publisher_code[0]) &&
|
||||
ISALNUM(romHeader.snes.ext.new_publisher_code[1]))
|
||||
if (ISALNUM(romHeader.snes.ext.new_publisher_code.c[0]) &&
|
||||
ISALNUM(romHeader.snes.ext.new_publisher_code.c[1]))
|
||||
{
|
||||
const array<char, 3> s_pub_code = {{
|
||||
romHeader.snes.ext.new_publisher_code[0],
|
||||
romHeader.snes.ext.new_publisher_code[1],
|
||||
romHeader.snes.ext.new_publisher_code.c[0],
|
||||
romHeader.snes.ext.new_publisher_code.c[1],
|
||||
'\0'
|
||||
}};
|
||||
s_publisher = fmt::format(FRUN(C_("RomData", "Unknown ({:s})")), s_pub_code.data());
|
||||
} else {
|
||||
s_publisher = fmt::format(FRUN(C_("RomData", "Unknown ({:0>2X} {:0>2X})")),
|
||||
static_cast<uint8_t>(romHeader.snes.ext.new_publisher_code[0]),
|
||||
static_cast<uint8_t>(romHeader.snes.ext.new_publisher_code[1]));
|
||||
static_cast<uint8_t>(romHeader.snes.ext.new_publisher_code.c[0]),
|
||||
static_cast<uint8_t>(romHeader.snes.ext.new_publisher_code.c[1]));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -782,6 +790,138 @@ string SNESPrivate::getGameID(bool doFake) const
|
||||
return gameID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Nintendo Power fields.
|
||||
* @return 0 on success; negative POSIX error code on error.
|
||||
*/
|
||||
int SNESPrivate::addFields_NP(void)
|
||||
{
|
||||
// Read the directory.
|
||||
typedef array<SNES_NP_DirEntry, 8> SNES_NP_Directory;
|
||||
unique_ptr<SNES_NP_Directory> directory(new SNES_NP_Directory);
|
||||
size_t size = file->seekAndRead(SNES_NP_DIRECTORY_ADDRESS, directory.get(), sizeof(SNES_NP_Directory));
|
||||
if (size != sizeof(SNES_NP_Directory)) {
|
||||
// Seek and/or read error. Skip the directory.
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
// Verify File0.
|
||||
const SNES_NP_DirEntry &entry0 = (*(directory.get()))[0];
|
||||
if (entry0.directory_index != 0 || memcmp(entry0.multicassette, SNES_NP_FILE0_FOOTER, 16) != 0) {
|
||||
// File0 is incorrect.
|
||||
// Not a Nintendo Power cartridge.
|
||||
return -ENOENT;
|
||||
}
|
||||
|
||||
// Process all of the files.
|
||||
fields.addTab("NP");
|
||||
auto *const vv_np = new RomFields::ListData_t();
|
||||
vv_np->reserve(directory->size());
|
||||
for (const SNES_NP_DirEntry &entry : *(directory.get())) {
|
||||
if (entry.directory_index == 0xFF) {
|
||||
// Unused directory index.
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: Is this okay, or should we resize vv_np and
|
||||
// get a reference to the new row that way?
|
||||
vector<string> data_row;
|
||||
data_row.reserve(5);
|
||||
|
||||
// #
|
||||
data_row.push_back(fmt::to_string(entry.directory_index));
|
||||
|
||||
// Title
|
||||
data_row.push_back(cp1252_sjis_to_utf8(
|
||||
entry.title_sjis, static_cast<int>(sizeof(entry.title_sjis))));
|
||||
|
||||
// Game Code
|
||||
// TODO: Trim trailing spaces?
|
||||
data_row.push_back(latin1_to_utf8(
|
||||
entry.game_code, static_cast<int>(sizeof(entry.game_code))));
|
||||
|
||||
// Timestamp
|
||||
// NOTE: Should probably be localized using Japanese timezone offsets,
|
||||
// but for now, we'll handle it as "UTC".
|
||||
|
||||
// Convert from strings to struct tm.
|
||||
time_t nptime = -1;
|
||||
struct tm tm;
|
||||
char buf[16];
|
||||
do {
|
||||
// Try to convert the date portion.
|
||||
memcpy(buf, entry.date, sizeof(entry.date));
|
||||
buf[sizeof(entry.date)] = '\0';
|
||||
// Try "MM/DD/YYYY" first.
|
||||
int c = sscanf(buf, "%02d/%02d/%04d", &tm.tm_mon, &tm.tm_mday, &tm.tm_year);
|
||||
if (c != 3) {
|
||||
// Try "YYYY/MM/DD" next.
|
||||
c = sscanf(buf, "%04d/%02d/%02d", &tm.tm_year, &tm.tm_mon, &tm.tm_mday);
|
||||
if (c != 3) {
|
||||
// Invalid date.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to convert the time portion.
|
||||
memcpy(buf, entry.time, sizeof(entry.time));
|
||||
buf[sizeof(entry.time)] = '\0';
|
||||
c = sscanf(buf, "%02d:%02d:%02d", &tm.tm_hour, &tm.tm_min, &tm.tm_sec);
|
||||
if (c != 3) {
|
||||
// Invalid time.
|
||||
break;
|
||||
}
|
||||
|
||||
// Adjust values.
|
||||
tm.tm_year -= 1900;
|
||||
tm.tm_mon -= 1;
|
||||
|
||||
// tm_wday and tm_yday are output variables.
|
||||
tm.tm_wday = 0;
|
||||
tm.tm_yday = 0;
|
||||
tm.tm_isdst = 0;
|
||||
|
||||
// If conversion fails, nptime will be set to -1.
|
||||
nptime = timegm(&tm);
|
||||
} while (0);
|
||||
|
||||
// Pack the 64-bit time_t into a string.
|
||||
RomFields::TimeString_t time_string;
|
||||
time_string.time = nptime;
|
||||
data_row.emplace_back(time_string.str, sizeof(time_string.str));
|
||||
|
||||
// Kiosk ID
|
||||
data_row.push_back(latin1_to_utf8(
|
||||
entry.kiosk_id, static_cast<int>(sizeof(entry.kiosk_id))));
|
||||
|
||||
// Add the row.
|
||||
vv_np->push_back(std::move(data_row));
|
||||
}
|
||||
|
||||
static const array<const char*, 5> np_headers = {{
|
||||
NOP_C_("SNES|NintendoPower", "#"),
|
||||
NOP_C_("SNES|NintendoPower", "Title"),
|
||||
NOP_C_("SNES|NintendoPower", "Game Code"),
|
||||
NOP_C_("SNES|NintendoPower", "Timestamp"),
|
||||
NOP_C_("SNES|NintendoPower", "Kiosk ID"),
|
||||
}};
|
||||
vector<string> *const v_pn_headers = RomFields::strArrayToVector_i18n(
|
||||
"SNES|NintendoPower", np_headers);
|
||||
|
||||
RomFields::AFLD_PARAMS params(RomFields::RFT_LISTDATA_SEPARATE_ROW, 8);
|
||||
params.col_attrs.is_timestamp = (1U << 3);
|
||||
params.col_attrs.dtflags = static_cast<RomFields::DateTimeFlags>(
|
||||
RomFields::RFT_DATETIME_HAS_DATE |
|
||||
RomFields::RFT_DATETIME_HAS_TIME |
|
||||
RomFields::RFT_DATETIME_IS_UTC);
|
||||
params.headers = v_pn_headers;
|
||||
params.data.single = vv_np;
|
||||
fields.addField_listData(C_("RomData", "Directory"), ¶ms);
|
||||
|
||||
// Fields added successfully.
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** SNES **/
|
||||
|
||||
/**
|
||||
@ -1208,7 +1348,7 @@ int SNES::loadFieldData(void)
|
||||
|
||||
// ROM header is read in the constructor.
|
||||
const SNES_RomHeader *const romHeader = &d->romHeader;
|
||||
d->fields.reserve(8); // Maximum of 8 fields.
|
||||
d->fields.reserve(9); // Maximum of 9 fields.
|
||||
|
||||
// Cartridge HW
|
||||
// TODO: Make this translatable.
|
||||
@ -1301,6 +1441,9 @@ int SNES::loadFieldData(void)
|
||||
|
||||
/** Add the field data. **/
|
||||
|
||||
// Tab name, in case we add a second tab.
|
||||
d->fields.setTabName(0, (romHeader->snes.destination_code == SNES_DEST_JAPAN) ? "SFC" : "SNES");
|
||||
|
||||
// Title
|
||||
d->fields.addField_string(C_("RomData", "Title"), d->getRomTitle());
|
||||
|
||||
@ -1502,6 +1645,16 @@ int SNES::loadFieldData(void)
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Is this a Nintendo Power cartridge?
|
||||
if (romHeader->snes.old_publisher_code == 0x33 &&
|
||||
romHeader->snes.destination_code == SNES_DEST_JAPAN &&
|
||||
romHeader->snes.ext.new_publisher_code.u16 == cpu_to_be16('01') &&
|
||||
romHeader->snes.ext.id4.u32 == cpu_to_be32('MENU'))
|
||||
{
|
||||
// This is a Nintendo Power cartridge.
|
||||
d->addFields_NP();
|
||||
}
|
||||
|
||||
// TODO: Other fields.
|
||||
|
||||
// Finished reading the field data.
|
||||
|
@ -117,7 +117,10 @@ typedef struct _SNES_RomHeader {
|
||||
struct {
|
||||
/** Extended header is only present if old_publisher_code == 0x33. **/
|
||||
struct {
|
||||
char new_publisher_code[2]; // [0x7FB0]
|
||||
union RP_PACKED {
|
||||
char c[2];
|
||||
uint16_t u16;
|
||||
} new_publisher_code; // [0x7FB0]
|
||||
#pragma pack(1)
|
||||
union RP_PACKED {
|
||||
char c[4];
|
||||
@ -245,6 +248,35 @@ typedef enum {
|
||||
SNES_BSX_PRG_SA_1 = 0x00000200, // SA-1 program
|
||||
} SNES_BSX_Program_Type;
|
||||
|
||||
/**
|
||||
* Nintendo Power directory entry
|
||||
* Reference: https://problemkaputt.de/fullsnes.htm#snescartnintendopowerdirectory
|
||||
*
|
||||
* All fields are in little-endian.
|
||||
*/
|
||||
#define SNES_NP_DIRECTORY_ADDRESS 0x60000U
|
||||
#define SNES_NP_FILE0_FOOTER "MULTICASSETTE 32"
|
||||
#pragma pack(1)
|
||||
typedef struct RP_PACKED _SNES_NP_DirEntry {
|
||||
uint8_t directory_index; // [0x0000] Directory index: 0-7 (or 0xFF for unused)
|
||||
uint8_t first_flash_block; // [0x0001] First 512K FLASH block (0-7 for blocks 0-7)
|
||||
uint8_t first_sram_block; // [0x0002] First 2K SRAM block (0-15 for blocks 0-15)
|
||||
uint16_t num_flash_blocks; // [0x0003] Number of 512K FLASH blocks (x4)
|
||||
uint16_t num_sram_blocks; // [0x0005] Number of 2K SRAM blocks (x16)
|
||||
char game_code[12]; // [0x0007] Game code, e.g. "SHVC-AxxJ- ")
|
||||
char title_sjis[44]; // [0x0013] Title in Shift-JIS, NULL-padded (not used by the menu program)
|
||||
uint8_t title_bmp[384]; // [0x003F] Title in bitmap format (192x12)
|
||||
char date[10]; // [0x01BF] Date ("MM/DD/YYYY" on LAW carts; "YYYY/MM/DD" on NIN carts)
|
||||
char time[8]; // [0x01C9] Time ("HH:MM:SS")
|
||||
char kiosk_id[8]; // [0x01D1] Kiosk ID:
|
||||
// - "LAWnnnnn" for Lawson Convenience Store kiosks
|
||||
// - "NINnnnnn" for titles pre-installed by Nintendo
|
||||
uint8_t unused[7703]; // [0x01D9] Unused (0xFF-filled)
|
||||
char multicassette[16]; // [0x1FF0] File0 contains "MULTICASSETTE 32"; others have 0xFF.
|
||||
} SNES_NP_DirEntry;
|
||||
ASSERT_STRUCT(SNES_NP_DirEntry, 0x2000);
|
||||
#pragma pack()
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
Loading…
Reference in New Issue
Block a user