这一篇说说跟NES文件格式相关的内容。
NES文件,在真机上相当于就是游戏卡带了。
下面是NES文件格式的说明表。
偏移 |
字节数 |
内容 |
0-3 |
4 |
字符串“NES^Z”用来识别.NES文件 |
4 |
1 |
16kB ROM的数目 |
5 |
1 |
8kB VROM的数目 |
6 |
1 |
D0:1=垂直镜像,0=水平镜像 |
D1:1=有电池记忆,SRAM地址$6000-$7FFF |
||
D2:1=在$7000-$71FF有一个512字节的trainer |
||
D3:1=4屏幕VRAM布局 |
||
D4-D7:ROM Mapper的低4位 |
||
7 |
1 |
D0-D3:保留,必须是0(准备作为副Mapper号^_^) |
D4-D7:ROM Mapper的高4位 |
||
8-F |
8 |
保留,必须是0 |
16- |
16KxM |
ROM段升序排列,如果存在trainer,它的512字节摆在ROM段之前 |
-EOF |
8KxN |
VROM段, 升序排列 |
结合代码来看看程序中是如何读取NES文件的。
代码所在的文件:NES\ROM.cpp ROM.h
所在函数:ROM::ROM( constchar* fname )
代码比较长,所以分开来一部分一部分的看,有些无关紧要,或者和NES文件格式主题关系不大的代码,我就略过了。
FILE *fp = NULL; LPBYTE temp = NULL; LPBYTE bios = NULL; LONG FileSize; ZEROMEMORY( &header, sizeof(header) ); ZEROMEMORY( path, sizeof(path) ); ZEROMEMORY( name, sizeof(name) ); bPAL = FALSE; bNSF = FALSE; NSF_PAGE_SIZE = 0; lpPRG = lpCHR = lpTrainer = lpDiskBios= lpDisk = NULL; crc = crcall = 0; mapper = 0; diskno = 0;
一些变量的初始化,具体含义用到了再详细研究。
if( !(fp = ::fopen( fname, "rb")) ) { LPCSTR szErrStr = CApp::GetErrorString( IDS_ERROR_OPEN ); ::wsprintf( szErrorString,szErrStr, fname ); throw szErrorString; }
打开文件,fp是文件句柄
::fseek( fp, 0,SEEK_END ); FileSize = ::ftell(fp ); ::fseek( fp, 0,SEEK_SET );
求文件大小,赋值给FileSize
if( FileSize < 17 ) { throw CApp::GetErrorString(IDS_ERROR_SMALLFILE ); }
NES文件头大小16,文件中至少得有个文件头才好继续。
if( !(temp = (LPBYTE)::malloc( FileSize )) ) { throw CApp::GetErrorString( IDS_ERROR_OUTOFMEMORY); } if( ::fread( temp, FileSize, 1, fp ) != 1 ) { throw CApp::GetErrorString( IDS_ERROR_READ ); } FCLOSE( fp );
文件中的所有内容读取出来放到temp缓冲区中,之后关闭文件。
::memcpy(&header, temp, sizeof(NESHEADER) );
读取NES文件头,存储到header中。NESHEADER定义如下:
可以结合文章开头的表格查看。
typedefstruct tagNESHEADER{ BYTE ID[4]; //NES文件标记 BYTE PRG_PAGE_SIZE; //16kB ROM的数目就是ROM大小 BYTE CHR_PAGE_SIZE; //8kB VROM的数目就是VROM大小 BYTE control1; BYTE control2; BYTE reserved[8]; //保留,必须是0 } NESHEADER;
DWORD PRGoffset, CHRoffset; //Rom和VRom数据距离文件头的偏移量 LONG PRGsize, CHRsize; //Rom和VRom的大小
根据文件类型的不同(NES,FDS等),接下来的代码产生了分支,我就只管NES了(NES还没搞明白,其它就暂时无视吧)。
if( header.ID[0] == 'N'&& header.ID[1] == 'E' && header.ID[2] == 'S' && header.ID[3] == 0x1A ) { PRGsize =(LONG)header.PRG_PAGE_SIZE*0x4000; CHRsize =(LONG)header.CHR_PAGE_SIZE*0x2000; PRGoffset = sizeof(NESHEADER); //ROM数据紧跟在文件头后面 CHRoffset =PRGoffset + PRGsize; //VROM数据紧跟在ROM数据后面 if( PRGsize <= 0 || (PRGsize+CHRsize) >FileSize ) { throw CApp::GetErrorString(IDS_ERROR_INVALIDNESHEADER ); } if( !(lpPRG = (LPBYTE)malloc( PRGsize )) ) { throw CApp::GetErrorString(IDS_ERROR_OUTOFMEMORY ); } ::memcpy( lpPRG,temp+PRGoffset, PRGsize ); if( CHRsize > 0 ) { if( !(lpCHR = (LPBYTE)malloc( CHRsize )) ) { throw CApp::GetErrorString(IDS_ERROR_OUTOFMEMORY ); } if( FileSize >= CHRoffset+CHRsize ) { memcpy(lpCHR, temp+CHRoffset, CHRsize ); } else { CHRsize-= (CHRoffset+CHRsize - FileSize); memcpy(lpCHR, temp+CHRoffset, CHRsize ); } } else { lpCHR =NULL; } }
以上一长串的代码,说白了就是,检查文件数据有没有异常,没有异常就读取ROM和VROM。
string tempstr; tempstr =CPathlib::SplitPath( fname ); ::strcpy( path,tempstr.c_str() ); tempstr =CPathlib::SplitFname( fname ); ::strcpy( name,tempstr.c_str() ); ::strcpy( fullpath, fname );
path保存NES文件所在的目录
name保存NES文件名
fullpath 保存NES文件的全路径
接下来的代码处理的是和mapper有关的东西。mapper这玩意儿水比较深,况且和NES文件格式这个主题关系不大,今后再细细研究吧。