NES: Added support for .qd format FDS roms

This commit is contained in:
Sour 2025-02-07 21:06:17 +09:00
parent 031cb7058c
commit 30fd76d9e0
14 changed files with 121 additions and 49 deletions

View file

@ -5,18 +5,13 @@
class BaseLoader
{
protected:
bool _checkOnly;
void Log(string message)
{
if(!_checkOnly) {
MessageManager::Log(message);
}
MessageManager::Log(message);
}
public:
BaseLoader(bool checkOnly = false)
BaseLoader()
{
_checkOnly = checkOnly;
}
};

View file

@ -10,37 +10,78 @@
#include "Utilities/FolderUtilities.h"
#include "Utilities/CRC32.h"
#include "Utilities/sha1.h"
#include "Utilities/HexUtilities.h"
#include "Utilities/StringUtilities.h"
void FdsLoader::AddGaps(vector<uint8_t>& diskSide, uint8_t * readBuffer)
FdsLoader::FdsLoader(bool useQdFormat) : BaseLoader()
{
_useQdFormat = useQdFormat;
}
int FdsLoader::GetSideCapacity()
{
return _useQdFormat ? QdDiskSideCapacity : FdsDiskSideCapacity;
}
void FdsLoader::AddGaps(vector<uint8_t>& diskSide, uint8_t* readBuffer, uint32_t bufferSize)
{
//Start image with 28300 bits of gap
diskSide.insert(diskSide.end(), 28300 / 8, 0);
for(size_t j = 0; j < FdsDiskSideCapacity;) {
uint8_t blockType = readBuffer[j];
auto read = [&](int i) -> uint8_t {
if(i >= 0 && i < bufferSize) {
return readBuffer[i];
}
return 0;
};
for(int j = 0; j < GetSideCapacity();) {
uint8_t blockType = read(j);
uint32_t blockLength = 1;
switch(blockType) {
case 1: blockLength = 56; break; //Disk header
case 2: blockLength = 2; break; //File count
case 3: blockLength = 16; break; //File header
case 4: blockLength = 1 + readBuffer[j - 3] + readBuffer[j - 2] * 0x100; break;
case 3:
//File header
//Log("ID: $" + HexUtilities::ToHex(read(j + 1)) + " - $" + HexUtilities::ToHex(read(j + 2)) + " - " + StringUtilities::GetString(readBuffer + j + 3, 8));
//Log("Size: $" + HexUtilities::ToHex(read(j + 0x0D) | (read(j+0x0E) << 8)));
//Log("Load address: $" + HexUtilities::ToHex(read(j + 0x0B) | (read(j + 0x0C) << 8)));
blockLength = 16;
break;
case 4:
if(_useQdFormat) {
blockLength = 1 + (read(j - 5) | (read(j - 4) << 8));
} else {
blockLength = 1 + (read(j - 3) | (read(j - 2) << 8));
}
break;
default: return; //End parsing when we encounter an invalid block type (This is what Nestopia apppears to do)
}
if(blockType == 0) {
diskSide.push_back(blockType);
} else {
diskSide.push_back(0x80);
diskSide.insert(diskSide.end(), &readBuffer[j], &readBuffer[j] + blockLength);
if(_useQdFormat) {
//Use CRC values from QD file
blockLength += 2;
}
if(j + blockLength >= bufferSize) {
return;
}
diskSide.push_back(0x80);
diskSide.insert(diskSide.end(), &readBuffer[j], &readBuffer[j] + blockLength);
if(!_useQdFormat) {
//Fake CRC value
diskSide.push_back(0x4D);
diskSide.push_back(0x62);
//Insert 976 bits of gap after a block
diskSide.insert(diskSide.end(), 976 / 8, 0);
}
//Insert 976 bits of gap after a block
diskSide.insert(diskSide.end(), 976 / 8, 0);
j += blockLength;
}
}
@ -48,7 +89,7 @@ void FdsLoader::AddGaps(vector<uint8_t>& diskSide, uint8_t * readBuffer)
vector<uint8_t> FdsLoader::RebuildFdsFile(vector<vector<uint8_t>> diskData, bool needHeader)
{
vector<uint8_t> output;
output.reserve(diskData.size() * FdsDiskSideCapacity + 16);
output.reserve(diskData.size() * GetSideCapacity() + 16);
if(needHeader) {
uint8_t header[16] = { 'F', 'D', 'S', '\x1a', (uint8_t)diskData.size(), '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0' };
@ -58,7 +99,7 @@ vector<uint8_t> FdsLoader::RebuildFdsFile(vector<vector<uint8_t>> diskData, bool
for(vector<uint8_t> &diskSide : diskData) {
bool inGap = true;
size_t i = 0, len = diskSide.size();
size_t gapNeeded = FdsDiskSideCapacity;
size_t gapNeeded = GetSideCapacity();
uint32_t fileSize = 0;
while(i < len) {
if(inGap) {
@ -74,10 +115,18 @@ vector<uint8_t> FdsLoader::RebuildFdsFile(vector<vector<uint8_t>> diskData, bool
case 3: blockLength = 16; fileSize = diskSide[i + 13] + diskSide[i + 14] * 0x100; break; //File header
case 4: blockLength = 1 + fileSize; break;
}
if(_useQdFormat) {
blockLength += 2;
}
output.insert(output.end(), &diskSide[i], &diskSide[i] + blockLength);
gapNeeded -= blockLength;
i += blockLength;
i += 2; //Skip CRC after block
if(!_useQdFormat) {
i += 2; //Skip CRC after block
}
inGap = true;
}
@ -97,7 +146,7 @@ void FdsLoader::LoadDiskData(vector<uint8_t>& romFile, vector<vector<uint8_t>>&
numberOfSides = romFile[4];
fileOffset = 16;
} else {
numberOfSides = (uint8_t)(romFile.size() / 65500);
numberOfSides = (uint8_t)(romFile.size() / GetSideCapacity());
}
for(uint32_t i = 0; i < numberOfSides; i++) {
@ -106,12 +155,12 @@ void FdsLoader::LoadDiskData(vector<uint8_t>& romFile, vector<vector<uint8_t>>&
diskHeaders.push_back(vector<uint8_t>(romFile.data() + fileOffset + 1, romFile.data() + fileOffset + 57));
AddGaps(fdsDiskImage, &romFile[fileOffset]);
fileOffset += FdsDiskSideCapacity;
AddGaps(fdsDiskImage, &romFile[fileOffset], romFile.size() - fileOffset);
fileOffset += GetSideCapacity();
//Ensure the image is 65500 bytes
if(fdsDiskImage.size() < FdsDiskSideCapacity) {
fdsDiskImage.resize(FdsDiskSideCapacity);
//Ensure the image is at least the expected size of a disk
if(fdsDiskImage.size() < GetSideCapacity()) {
fdsDiskImage.resize(GetSideCapacity());
}
}
}

View file

@ -8,12 +8,17 @@ class FdsLoader : public BaseLoader
{
private:
static constexpr size_t FdsDiskSideCapacity = 65500;
static constexpr size_t QdDiskSideCapacity = 65536;
bool _useQdFormat = false;
private:
void AddGaps(vector<uint8_t>& diskSide, uint8_t* readBuffer);
void AddGaps(vector<uint8_t>& diskSide, uint8_t* readBuffer, uint32_t bufferSize);
int GetSideCapacity();
public:
using BaseLoader::BaseLoader;
FdsLoader(bool useQdFormat = false);
vector<uint8_t> RebuildFdsFile(vector<vector<uint8_t>> diskData, bool needHeader);
void LoadDiskData(vector<uint8_t>& romFile, vector<vector<uint8_t>> &diskData, vector<vector<uint8_t>> &diskHeaders);

View file

@ -191,14 +191,10 @@ void UnifLoader::LoadRom(RomData& romData, vector<uint8_t>& romFile, bool databa
Log("[UNIF] Mirroring: " + mirroringType);
Log("[UNIF] Battery: " + string(romData.Info.HasBattery ? "Yes" : "No"));
if(!_checkOnly) {
GameDatabase::SetGameInfo(romData.Info.Hash.PrgChrCrc32, romData, databaseEnabled, false);
}
GameDatabase::SetGameInfo(romData.Info.Hash.PrgChrCrc32, romData, databaseEnabled, false);
if(romData.Info.MapperID == UnifBoards::UnknownBoard) {
if(!_checkOnly) {
MessageManager::DisplayMessage("Error", "UnsupportedMapper", "UNIF: " + _mapperName);
}
MessageManager::DisplayMessage("Error", "UnsupportedMapper", "UNIF: " + _mapperName);
romData.Error = true;
}
}

View file

@ -142,7 +142,7 @@ void iNesLoader::LoadRom(RomData& romData, vector<uint8_t>& romFile, NesHeader *
Log("[iNes] EPSM: Yes");
}
if(!_checkOnly && !romData.Info.IsNes20Header) {
if(!romData.Info.IsNes20Header) {
GameDatabase::SetGameInfo(romData.Info.Hash.PrgChrCrc32, romData, databaseEnabled, preloadedHeader != nullptr);
}

View file

@ -15,6 +15,7 @@
#include "Shared/FirmwareHelper.h"
#include "Utilities/Patches/IpsPatcher.h"
#include "Utilities/Serializer.h"
#include "Utilities/StringUtilities.h"
void Fds::InitMapper()
{
@ -48,8 +49,10 @@ void Fds::InitMapper(RomData &romData)
_fdsDiskSides = romData.FdsDiskData;
_fdsDiskHeaders = romData.FdsDiskHeaders;
_fdsRawData = romData.RawData;
string filename = StringUtilities::ToLower(romData.Info.Filename);
_useQdFormat = StringUtilities::EndsWith(filename, ".qd");
FdsLoader loader;
FdsLoader loader(_useQdFormat);
loader.LoadDiskData(_fdsRawData, _orgDiskSides, _orgDiskHeaders);
//Apply save data (saved as an IPS file), if found
@ -69,7 +72,7 @@ void Fds::LoadDiskData(vector<uint8_t> ipsData)
_fdsDiskSides.clear();
_fdsDiskHeaders.clear();
FdsLoader loader;
FdsLoader loader(_useQdFormat);
vector<uint8_t> patchedData;
if(ipsData.size() > 0 && IpsPatcher::PatchBuffer(ipsData, _fdsRawData, patchedData)) {
loader.LoadDiskData(patchedData, _fdsDiskSides, _fdsDiskHeaders);
@ -80,9 +83,9 @@ void Fds::LoadDiskData(vector<uint8_t> ipsData)
vector<uint8_t> Fds::CreateIpsPatch()
{
FdsLoader loader;
FdsLoader loader(_useQdFormat);
bool needHeader = (memcmp(_fdsRawData.data(), "FDS\x1a", 4) == 0);
vector<uint8_t> newData = loader.RebuildFdsFile(_fdsDiskSides, needHeader);
vector<uint8_t> newData = loader.RebuildFdsFile(_fdsDiskSides, needHeader);
return IpsPatcher::CreatePatch(_fdsRawData, newData);
}

View file

@ -67,6 +67,7 @@ private:
bool _gapEnded = true;
bool _scanningDisk = false;
bool _transferComplete = false;
bool _useQdFormat = false;
vector<uint8_t> _fdsRawData;
vector<vector<uint8_t>> _fdsDiskSides;

View file

@ -65,7 +65,7 @@ public:
NesConsole(Emulator* emulator);
~NesConsole();
static vector<string> GetSupportedExtensions() { return { ".nes", ".fds", ".unif", ".unf", ".nsf", ".nsfe", ".studybox" }; }
static vector<string> GetSupportedExtensions() { return { ".nes", ".fds", ".unif", ".unf", ".nsf", ".nsfe", ".studybox", ".qd" }; }
static vector<string> GetSupportedSignatures() { return { "NES\x1a", "FDS\x1a", "\x1*NINTENDO-HVC*", "NESM\x1a", "NSFE", "UNIF", "STBX" }; }
NesCpu* GetCpu() { return _cpu.get(); }

View file

@ -70,6 +70,7 @@ namespace Mesen.Config
CreateMimeType("x-mesen-nes", "nes", "NES ROM", mimeTypes, cfg.AssociateNesRomFiles);
CreateMimeType("x-mesen-fds", "fds", "FDS ROM", mimeTypes, cfg.AssociateNesRomFiles);
CreateMimeType("x-mesen-qd", "qd", "FDS ROM (QD format)", mimeTypes, cfg.AssociateNesRomFiles);
CreateMimeType("x-mesen-studybox", "studybox", "Studybox ROM (Famicom)", mimeTypes, cfg.AssociateNesRomFiles);
CreateMimeType("x-mesen-unif", "unf", "NES ROM", mimeTypes, cfg.AssociateNesRomFiles);
@ -191,6 +192,7 @@ namespace Mesen.Config
FileAssociationHelper.UpdateFileAssociation("nes", cfg.AssociateNesRomFiles);
FileAssociationHelper.UpdateFileAssociation("fds", cfg.AssociateNesRomFiles);
FileAssociationHelper.UpdateFileAssociation("qd", cfg.AssociateNesRomFiles);
FileAssociationHelper.UpdateFileAssociation("unf", cfg.AssociateNesRomFiles);
FileAssociationHelper.UpdateFileAssociation("studybox", cfg.AssociateNesRomFiles);

View file

@ -710,7 +710,7 @@
<Control ID="grpFileAssociations">File Associations</Control>
<Control ID="chkSnesRomFiles">SNES roms: .sfc, .smc, .swc, .fig, .bs</Control>
<Control ID="chkSnesMusicFiles">SNES music: .spc</Control>
<Control ID="chkNesRomFiles">NES roms: .nes, .unif, .fds, .studybox</Control>
<Control ID="chkNesRomFiles">NES roms: .nes, .unif, .fds, .qd, .studybox</Control>
<Control ID="chkNesMusicFiles">NES music: .nsf, .nsfe</Control>
<Control ID="chkGbRomFiles">Game Boy roms: .gb, .gbc, .gbx</Control>
<Control ID="chkGbMusicFiles">Game Boy music: .gbs</Control>

View file

@ -53,7 +53,7 @@ namespace Mesen.Utilities
if(ext == FileDialogHelper.RomExt) {
filter.Add(new FilePickerFileType("All ROM files") { Patterns = new List<string>() {
"*.sfc", "*.fig", "*.smc", "*.bs", "*.st", "*.spc",
"*.nes", "*.fds", "*.unif", "*.unf", "*.studybox", "*.nsf", "*.nsfe",
"*.nes", "*.fds", "*.qd", "*.unif", "*.unf", "*.studybox", "*.nsf", "*.nsfe",
"*.gb", "*.gbc", "*.gbx", "*.gbs",
"*.pce", "*.sgx", "*.cue", "*.hes",
"*.sms", "*.gg", "*.sg", "*.col",
@ -62,7 +62,7 @@ namespace Mesen.Utilities
"*.zip", "*.7z"
} });
filter.Add(new FilePickerFileType("SNES ROM files") { Patterns = new List<string>() { "*.sfc", "*.fig", "*.smc", "*.bs", "*.st", "*.spc" } });
filter.Add(new FilePickerFileType("NES ROM files") { Patterns = new List<string>() { "*.nes", "*.fds", "*.unif", "*.unf", "*.studybox", "*.nsf", "*.nsfe" } });
filter.Add(new FilePickerFileType("NES ROM files") { Patterns = new List<string>() { "*.nes", "*.fds", "*.qd", "*.unif", "*.unf", "*.studybox", "*.nsf", "*.nsfe" } });
filter.Add(new FilePickerFileType("GB ROM files") { Patterns = new List<string>() { "*.gb", "*.gbc", "*.gbx", "*.gbs" } });
filter.Add(new FilePickerFileType("GBA ROM files") { Patterns = new List<string>() { "*.gba" } });
filter.Add(new FilePickerFileType("PC Engine ROM files") { Patterns = new List<string>() { "*.pce", "*.sgx", "*.cue", "*.hes" } });

View file

@ -13,7 +13,7 @@ namespace Mesen.Utilities
private static HashSet<string> _romExtensions = new HashSet<string>() {
".sfc", ".smc", ".fig", ".swc", ".bs", ".st",
".gb", ".gbc", ".gbx",
".nes", ".unif", ".unf", ".fds", ".studybox",
".nes", ".unif", ".unf", ".fds", ".qd", ".studybox",
".pce", ".sgx", ".cue",
".sms", ".gg", ".sg", ".col",
".gba",

View file

@ -65,6 +65,10 @@ public:
static bool StartsWith(string& str, const char* content)
{
size_t length = strlen(content);
if(str.size() < length) {
return false;
}
for(size_t i = 0; i < length; i++) {
if(str[i] != content[i]) {
return false;
@ -72,7 +76,24 @@ public:
}
return true;
}
static bool EndsWith(string& str, const char* content)
{
size_t length = strlen(content);
if(str.size() < length) {
return false;
}
size_t startPos = str.size() - length;
for(size_t i = startPos; i < str.size(); i++) {
if(str[i] != content[i - startPos]) {
return false;
}
}
return true;
}
static bool Contains(string& str, const char* content)
{
size_t length = strlen(content);

View file

@ -12,7 +12,7 @@
#include "Utilities/CRC32.h"
const std::initializer_list<string> VirtualFile::RomExtensions = {
".nes", ".fds", ".unif", ".unf", ".nsf", ".nsfe", ".studybox",
".nes", ".fds", ".qd", ".unif", ".unf", ".nsf", ".nsfe", ".studybox",
".sfc", ".swc", ".fig", ".smc", ".bs", ".st", ".spc",
".gb", ".gbc", ".gbx", ".gbs",
".pce", ".sgx", ".cue", ".hes",