[libromdata] Intellivision: Initial Intellivision ROM image parser.

There's a bit of weirdness with Intellivision due to its unusual word
size. The General Instrument CP1610 is a 16-bit CPU, but uses 10-bit
opcodes. Because of this, Mattel used 10-bit ROMs. Some of the fields
in the ROM header use 16-bit addresses, split up into two 10-bit words.
The ROM file uses 16-bit big-endian for words, so in order to decode
these addresses, we have to take two 16-bit big-endian values, read
the low 8 bits of each, and combine it into a 16-bit value.

NOTE: ROM images must have a .int or .itv file extension, since the
Intellivision ROM header doesn't have a magic number.
This commit is contained in:
David Korth 2024-03-02 16:47:14 -05:00
parent 77c7f7fada
commit 13a95183eb
9 changed files with 479 additions and 0 deletions

View File

@ -46,6 +46,10 @@
D67, and (mostly supports) G64 and G71 images, plus GEOS file icons.
* ColecoVision: ColecoVision ROM images. Supports reading the title screen
message and copyright/release year from .col images, among other things.
Requires a .col file extension due to lack of magic number.
* Intellivision: Intellivision ROM images. Supports reading the game title
and copyright year (if present), and some flags. Requires a .int or .itv
file extension due to lack of magic number.
* New parser features:
* DMG: MMM01 and MBC1M multicarts are now detected, and the internal ROM

View File

@ -47,6 +47,9 @@ Major changes in v2.3 include:
* ColecoVision ROM images are now supported, including the text displayed
on the ColecoVision logo screen.
* Intellivision ROM images are now supported, including the game title
and copyright year.
* New translations: Romanian and Italian
Translators wanted; if you can translate rom-properties from English to another
@ -128,6 +131,7 @@ button.
|:-------------------------:|:--------------:|:--------:|:---------------:|:---------------:|
| ColecoVision | Yes | Yes | N/A | No |
| Commodore 64/128 .CRT | Yes | Yes | N/A | Title |
| Intellivision | Yes | Yes | N/A | No |
| iQue Player ticket files | Yes | Yes | Icon, Banner | No |
| Microsoft Xbox (XBE) | Yes | Yes | Icon | No |
| Microsoft Xbox 360 (XEX) | Yes | Yes | Icon | No |

View File

@ -19,6 +19,7 @@ SET(${PROJECT_NAME}_SRCS
Console/GameCubeBNR.cpp
Console/GameCubeSave.cpp
Console/GameCubeRegions.cpp
Console/Intellivision.cpp
Console/iQuePlayer.cpp
Console/MegaDrive.cpp
Console/MegaDriveRegions.cpp
@ -149,6 +150,7 @@ SET(${PROJECT_NAME}_H
Console/GameCubeBNR.hpp
Console/GameCubeSave.hpp
Console/GameCubeRegions.hpp
Console/Intellivision.hpp
Console/iQuePlayer.hpp
Console/MegaDrive.hpp
Console/MegaDriveRegions.hpp
@ -180,6 +182,7 @@ SET(${PROJECT_NAME}_H
Console/gcn_structs.h
Console/gcn_banner.h
Console/gcn_card.h
Console/intv_structs.h
Console/ique_player_structs.h
Console/md_structs.h
Console/n64_structs.h

View File

@ -0,0 +1,351 @@
/***************************************************************************
* ROM Properties Page shell extension. (libromdata) *
* Intellivision.cpp: Intellivision ROM reader. *
* *
* Copyright (c) 2016-2024 by David Korth. *
* SPDX-License-Identifier: GPL-2.0-or-later *
***************************************************************************/
#include "stdafx.h"
#include "Intellivision.hpp"
#include "intv_structs.h"
#include "ctypex.h"
// Other rom-properties libraries
using namespace LibRpBase;
using namespace LibRpFile;
using namespace LibRpText;
// C++ STL classes
using std::string;
using std::vector;
namespace LibRomData {
class IntellivisionPrivate final : public RomDataPrivate
{
public:
IntellivisionPrivate(const IRpFilePtr &file);
private:
typedef RomDataPrivate super;
RP_DISABLE_COPY(IntellivisionPrivate)
public:
/** RomDataInfo **/
static const char *const exts[];
static const char *const mimeTypes[];
static const RomDataInfo romDataInfo;
public:
// ROM header
Intellivision_ROMHeader romHeader;
public:
/**
* Get the title from the ROM header.
* @param pOutYear [out,opt] Release year, if set; otherwise, -1.
* @return Title screen lines, or empty string on error.
*/
string getTitle(int *pOutYear = nullptr) const;
};
ROMDATA_IMPL(Intellivision)
/** IntellivisionPrivate **/
/* RomDataInfo */
const char *const IntellivisionPrivate::exts[] = {
".int", ".itv",
//".bin", // NOTE: Too generic...
nullptr
};
const char *const IntellivisionPrivate::mimeTypes[] = {
// Unofficial MIME types.
// TODO: Get these upstreamed on FreeDesktop.org.
"application/x-intellivision-rom",
nullptr
};
const RomDataInfo IntellivisionPrivate::romDataInfo = {
"Intellivision", exts, mimeTypes
};
IntellivisionPrivate::IntellivisionPrivate(const IRpFilePtr &file)
: super(file, &romDataInfo)
{
// Clear the ROM header struct.
memset(&romHeader, 0, sizeof(romHeader));
}
/**
* Get the title from the ROM header.
* @param pOutYear [out,opt] Release year, if set; otherwise, -1.
* @return Title screen lines, or empty string on error.
*/
string IntellivisionPrivate::getTitle(int *pOutYear) const
{
// NOTE: The cartridge ROM is mapped to 0x5000.
// Title/date address must be between 0x5010 and 0x50FF.
uint16_t title_addr = romHeader.title_date.get_real_value();
if (title_addr < 0x5010 || title_addr >= (0x5000 + ARRAY_SIZE(romHeader.u16))) {
// Out of range.
return {};
}
// Convert to an absolute address.
title_addr -= 0x5000;
// First word has the year, minus 1900.
// NOTE: ROMs that don't have a valid title/date field may have 0 (1900) here.
// Some homebrew titles have weird values, e.g. 2 (1902) or 4 (1904), so
// we'll allow any year as long as it's not 0 (1900).
if (pOutYear) {
unsigned int year_raw = be16_to_cpu(romHeader.u16[title_addr]);
if (year_raw != 0) {
*pOutYear = year_raw + 1900;
}
}
// Title is a NULL-terminated ASCII string, but it's 16-bit words.
// Convert it to 8-bit ASCII.
// NOTE: Removing the high bit to ensure UTF-8 compatibility.
// TODO: Verify the whole EXEC character set.
string title;
title.reserve(32);
const uint16_t *const p_end = &romHeader.u16[ARRAY_SIZE(romHeader.u16)];
for (const uint16_t *p = &romHeader.u16[title_addr+1]; p < p_end; p++) {
const uint16_t chr = *p;
if (chr == 0) {
break;
}
title += static_cast<char>(static_cast<uint8_t>(be16_to_cpu(chr)));
}
// Trim the title.
// NOTE: Games that don't use EXEC don't necessarily have a valid title.
// The title field is usually single space in that case, and we should
// ignore it. (date is 0 aka 1900; maybe we should skip if that's the case?)
while (!title.empty() && ISSPACE(title[title.size()-1])) {
title.resize(title.size()-1);
}
return title;
}
/** Intellivision **/
/**
* Read a Intellivision ROM image.
*
* A ROM image must be opened by the caller. The file handle
* will be ref()'d and must be kept open in order to load
* data from the ROM image.
*
* To close the file, either delete this object or call close().
*
* NOTE: Check isValid() to determine if this is a valid ROM.
*
* @param file Open ROM image.
*/
Intellivision::Intellivision(const IRpFilePtr &file)
: super(new IntellivisionPrivate(file))
{
RP_D(Intellivision);
if (!d->file) {
// Could not ref() the file handle.
return;
}
// Read the ROM header.
d->file->rewind();
size_t size = d->file->read(&d->romHeader, sizeof(d->romHeader));
if (size != sizeof(d->romHeader)) {
// Seek and/or read error.
d->file.reset();
return;
}
// Check if this ROM image is supported.
const char *const filename = file->filename();
const DetectInfo info = {
{0, sizeof(d->romHeader), reinterpret_cast<const uint8_t*>(&d->romHeader)},
FileSystem::file_ext(filename), // ext
0 // szFile (not needed for Intellivision)
};
d->isValid = (isRomSupported_static(&info) >= 0);
if (!d->isValid) {
d->file.reset();
}
}
/**
* Is a ROM image supported by this class?
* @param info DetectInfo containing ROM detection information.
* @return Class-specific system ID (>= 0) if supported; -1 if not.
*/
int Intellivision::isRomSupported_static(const DetectInfo *info)
{
assert(info != nullptr);
assert(info->ext != nullptr);
if (!info || !info->ext) {
// Needs the file extension...
return -1;
}
// File extension is required.
if (info->ext[0] == '\0') {
// Empty file extension...
return -1;
}
// The Intellivision ROM header doesn't have enough magic
// to conclusively determine if it's a Intellivision ROM,
// so check the file extension.
for (const char *const *ext = IntellivisionPrivate::exts;
*ext != nullptr; ext++)
{
if (!strcasecmp(info->ext, *ext)) {
// File extension is supported.
return 0;
}
}
// Not supported.
return -1;
}
/**
* Get the name of the system the loaded ROM is designed for.
* @param type System name type. (See the SystemName enum.)
* @return System name, or nullptr if type is invalid.
*/
const char *Intellivision::systemName(unsigned int type) const
{
RP_D(const Intellivision);
if (!d->isValid || !isSystemNameTypeValid(type))
return nullptr;
// Intellivision has the same name worldwide, so we can
// ignore the region selection.
static_assert(SYSNAME_TYPE_MASK == 3,
"N64::systemName() array index optimization needs to be updated.");
// Bits 0-1: Type. (long, short, abbreviation)
static const char *const sysNames[4] = {
"Intellivision", "Intellivision", "INTV", nullptr
};
return sysNames[type & SYSNAME_TYPE_MASK];
}
/**
* Load field data.
* Called by RomData::fields() if the field data hasn't been loaded yet.
* @return Number of fields read on success; negative POSIX error code on error.
*/
int Intellivision::loadFieldData(void)
{
RP_D(Intellivision);
if (!d->fields.empty()) {
// Field data *has* been loaded...
return 0;
} else if (!d->file) {
// File isn't open.
return -EBADF;
} else if (!d->isValid) {
// Unknown ROM image type.
return -EIO;
}
const Intellivision_ROMHeader *const romHeader = &d->romHeader;
d->fields.reserve(3); // Maximum of 3 fields.
// Title
int year = -1;
const string title = d->getTitle(&year);
if (!title.empty()) {
d->fields.addField_string(C_("RomData", "Title"), title);
}
// Copyright year
if (year >= 0) {
d->fields.addField_string_numeric(C_("Intellivision", "Copyright Year"), year);
}
// Flags
unsigned int flags = be16_to_cpu(romHeader->flags);
// If both "Skip ECS" bits aren't set, clear both to prevent issues.
if ((flags & INTV_SKIP_ECS) != INTV_SKIP_ECS) {
flags &= ~INTV_SKIP_ECS;
}
static const char *const flags_bitfield_names[] = {
// Bits 0-5: Keyclick bits (TODO)
nullptr, nullptr, nullptr, nullptr, nullptr,
// Bits 6-8
NOP_C_("Intellivision|Flags", "Intellivision 2"),
NOP_C_("Intellivision|Flags", "Run code after title string"),
NOP_C_("Intellivision|Flags", "Skip ECS title screen"),
};
vector<string> *const v_flags_bitfield_names = RomFields::strArrayToVector_i18n(
"Region", flags_bitfield_names, ARRAY_SIZE(flags_bitfield_names));
d->fields.addField_bitfield(C_("Intellivision", "Flags"),
v_flags_bitfield_names, 2, flags);
// TODO: Entry point (differs if EXEC is used or not)
// Finished reading the field data.
return static_cast<int>(d->fields.count());
}
/**
* Load metadata properties.
* Called by RomData::metaData() if the metadata hasn't been loaded yet.
* @return Number of metadata properties read on success; negative POSIX error code on error.
*/
int Intellivision::loadMetaData(void)
{
RP_D(Intellivision);
if (d->metaData != nullptr) {
// Metadata *has* been loaded...
return 0;
} else if (!d->file) {
// File isn't open.
return -EBADF;
} else if (!d->isValid) {
// Unknown ROM image type.
return -EIO;
}
// Create the metadata object.
d->metaData = new RomMetaData();
d->metaData->reserve(2); // Maximum of 2 metadata properties.
//const Intellivision_ROMHeader *const romHeader = &d->romHeader;
// Title
int year = -1;
const string title = d->getTitle(&year);
if (!title.empty()) {
d->metaData->addMetaData_string(Property::Title, title);
}
// Release year (actually copyright year)
if (year >= 0) {
d->metaData->addMetaData_uint(Property::ReleaseYear, static_cast<unsigned int>(year));
}
// Finished reading the metadata.
return (d->metaData ? static_cast<int>(d->metaData->count()) : -ENOENT);
}
}

View File

@ -0,0 +1,19 @@
/***************************************************************************
* ROM Properties Page shell extension. (libromdata) *
* Intellivision.hpp: Intellivision ROM reader. *
* *
* Copyright (c) 2016-2024 by David Korth. *
* SPDX-License-Identifier: GPL-2.0-or-later *
***************************************************************************/
#pragma once
#include "librpbase/RomData.hpp"
namespace LibRomData {
ROMDATA_DECL_BEGIN(Intellivision)
ROMDATA_DECL_METADATA()
ROMDATA_DECL_END()
}

View File

@ -0,0 +1,86 @@
/***************************************************************************
* ROM Properties Page shell extension. (libromdata) *
* intv_structs.h: Intellivision ROM image data structures. *
* *
* Copyright (c) 2016-2024 by David Korth. *
* SPDX-License-Identifier: GPL-2.0-or-later *
***************************************************************************/
#pragma once
#include "common.h"
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* Intellivision: BIDECLE type
* This type stores a low byte and high byte from a 16-bit word
* in two consecutive 10-bit words.
*
* All fields are in 16-bit big-endian, but the byte ordering
* within the fields is (low, high).
*/
typedef union _Intv_BIDECLE {
struct {
uint8_t unused_lo;
uint8_t lo;
uint8_t unused_hi;
uint8_t hi;
};
uint16_t u16[2];
#ifdef __cplusplus
uint16_t get_real_value(void) const {
return (hi << 8) | lo;
}
#endif /* __cplusplus */
} Intv_BIDECLE;
ASSERT_STRUCT(Intv_BIDECLE, 2*sizeof(uint16_t));
/**
* Intellivision ROM image file header.
* Reference: https://wiki.intellivision.us/index.php/Hello_World_Tutorial
*
* All fields are in 16-bit big-endian.
*
* NOTE: Intellivision used 10-bit ROMs. ROM images use
* 16-bit words for convenience, plus homebrew games
* sometimes use 16-bit ROMs.
*/
typedef union _Intellivision_ROMHeader {
struct {
// Pointers (NOTE: Addresses are in 16-bit word units.)
Intv_BIDECLE mob_picture_base; // [0x000] MOB picture base
Intv_BIDECLE process_table; // [0x004] Process table
Intv_BIDECLE program_start_address; // [0x008] Entry point (only used if EXEC is in use)
Intv_BIDECLE bkgnd_picture_base; // [0x00C] Background picture base
Intv_BIDECLE gram_pictures; // [0x010] GRAM pictures
Intv_BIDECLE title_date; // [0x014] Title and date (date is year minus 1900)
uint16_t flags; // [0x018] Flags (see Intellivision_Flags_e)
uint16_t screen_border_ctrl; // [0x01A] Screen border control
uint16_t color_stack_mode; // [0x01C] Color stack and framebuffer mode
uint16_t color_stack[4]; // [0x01E] Initial color stack
uint16_t border_color; // [0x026] Initial border color
};
uint16_t u16[256]; // Direct access for e.g. title/date
} Intellivision_ROMHeader;
ASSERT_STRUCT(Intellivision_ROMHeader, 512);
/**
* Intelliviison flags
*/
typedef enum {
INTV_SKIP_ECS = (1U << 9) | (1U << 8), // Skip ECS title screen (both bits must be set)
INTV_RUN_CODE_AFTER_TITLE = (1U << 7), // Run code that appears after the title string
INTV_SUPPORT_INTV2 = (1U << 6), // Must be set to allow use on Intellivision 2
INTV_KEYCLICK_MASK = 0x001F, // Keyclick mask (requires EXEC)
} Intellivision_Flags_e;
#ifdef __cplusplus
}
#endif

View File

@ -40,6 +40,7 @@ using std::vector;
#include "Console/GameCube.hpp"
#include "Console/GameCubeBNR.hpp"
#include "Console/GameCubeSave.hpp"
#include "Console/Intellivision.hpp"
#include "Console/iQuePlayer.hpp"
#include "Console/MegaDrive.hpp"
#include "Console/N64.hpp"
@ -378,6 +379,7 @@ const RomDataFactoryPrivate::RomDataFns RomDataFactoryPrivate::romDataFns_header
GetRomDataFns(GameCube, ATTR_HAS_THUMBNAIL | ATTR_HAS_METADATA | ATTR_SUPPORTS_DEVICES),
GetRomDataFns(GameCubeBNR, ATTR_HAS_THUMBNAIL | ATTR_HAS_METADATA),
GetRomDataFns(GameCubeSave, ATTR_HAS_THUMBNAIL | ATTR_HAS_METADATA),
GetRomDataFns(Intellivision, ATTR_HAS_METADATA),
GetRomDataFns(iQuePlayer, ATTR_HAS_THUMBNAIL | ATTR_HAS_METADATA),
// MegaDrive: ATTR_SUPPORTS_DEVICES for Sega CD
GetRomDataFns(MegaDrive, ATTR_HAS_THUMBNAIL | ATTR_HAS_METADATA | ATTR_SUPPORTS_DEVICES),

View File

@ -30,6 +30,7 @@ application/x-colecovision-rom # ColecoVision
application/x-n64-rom # N64
application/x-nes-rom # NES
application/x-fds-disk # NES
application/x-intellivision-rom # IntelliVision
application/x-ps1-executable # PlayStationEXE
application/x-sms-rom # Sega8Bit
application/x-gamegear-rom # Sega8Bit

View File

@ -365,6 +365,15 @@
</magic>
</mime-type>
<!-- Intellivision -->
<mime-type type="application/x-intellivision-rom">
<comment>Intellivision ROM image</comment>
<sub-class-of type="application/x-executable"/>
<generic-icon name="application-x-executable"/>
<glob pattern="*.int"/>
<glob pattern="*.itv"/>
</mime-type>
<!-- iQuePlayer -->
<!-- TODO: Distinguish between CMD (10,668 bytes) and DAT (11,084 bytes). -->
<mime-type type="application/x-ique-cmd">