From 85a7d3474b75365efb1bdcf69c6cd8c11bbb964f Mon Sep 17 00:00:00 2001 From: Asnivor Date: Tue, 15 Oct 2024 17:27:05 +0100 Subject: [PATCH] Identify correct core for IPF image Obviously we don't support IPF images right now, but with any luck we will eventually. So it makes sense to have something in place in the frontend to do core selection (when a gamedb hash is not found) based on the INFO block within the IPF file itself. --- src/BizHawk.Client.Common/RomLoader.cs | 2 +- .../Database/Database.cs | 8 +- src/BizHawk.Emulation.Common/IpfIdentifier.cs | 402 ++++++++++++++++++ 3 files changed, 409 insertions(+), 3 deletions(-) create mode 100644 src/BizHawk.Emulation.Common/IpfIdentifier.cs diff --git a/src/BizHawk.Client.Common/RomLoader.cs b/src/BizHawk.Client.Common/RomLoader.cs index bc332cbaf5..35c668d319 100644 --- a/src/BizHawk.Client.Common/RomLoader.cs +++ b/src/BizHawk.Client.Common/RomLoader.cs @@ -900,7 +900,7 @@ namespace BizHawk.Client.Common public static readonly IReadOnlyCollection WSWAN = new[] { "ws", "wsc", "pc2" }; - public static readonly IReadOnlyCollection ZXSpectrum = new[] { "tzx", "tap", "dsk", "pzx" }; + public static readonly IReadOnlyCollection ZXSpectrum = new[] { "tzx", "tap", "dsk", "pzx", "ipf" }; public static readonly IReadOnlyCollection AutoloadFromArchive = Array.Empty() .Concat(A26) diff --git a/src/BizHawk.Emulation.Common/Database/Database.cs b/src/BizHawk.Emulation.Common/Database/Database.cs index 623995586e..a4ef8a09ec 100644 --- a/src/BizHawk.Emulation.Common/Database/Database.cs +++ b/src/BizHawk.Emulation.Common/Database/Database.cs @@ -412,14 +412,18 @@ namespace BizHawk.Emulation.Common case ".ADF": case ".ADZ": - case ".DMS": - case ".IPF": + case ".DMS": case ".FDI": case ".HDF": case ".LHA": game.System = VSystemID.Raw.Amiga; break; + case ".IPF": + var ipfId = new IpfIdentifier(romData); + game.System = ipfId.IdentifiedSystem; + break; + case ".32X": game.System = VSystemID.Raw.Sega32X; game.AddOption("32X", "true"); diff --git a/src/BizHawk.Emulation.Common/IpfIdentifier.cs b/src/BizHawk.Emulation.Common/IpfIdentifier.cs new file mode 100644 index 0000000000..09dd1ab6f3 --- /dev/null +++ b/src/BizHawk.Emulation.Common/IpfIdentifier.cs @@ -0,0 +1,402 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; +using BizHawk.Common.StringExtensions; + + +namespace BizHawk.Emulation.Common +{ + /// + /// Even though we don't currently support IPF files, it makes sense for the future that we can identify them + /// (or more precisely, the core that we need to pass them too if an entry is not present in the gamedb) + /// The IPF INFO record does contain a platform entry that assists in this. + /// + public class IpfIdentifier + { + /// + /// Default fallthrough to Amiga + /// + public string IdentifiedSystem { get; set; } = VSystemID.Raw.Amiga; + + private readonly byte[] _data; + + public IpfIdentifier(byte[] imageData) + { + _data = imageData; + ParseIpfImage(); + } + + private void ParseIpfImage() + { + // look for standard magic string + string ident = Encoding.ASCII.GetString(_data, 0, 16); + + if (!ident.Contains("CAPS", StringComparison.OrdinalIgnoreCase)) + { + // incorrect format + return; + } + + int pos = 0; + + List blocks = new List(); + + while (pos < _data.Length) + { + try + { + var block = IPFBlock.ParseNextBlock(ref pos, _data, blocks); + + if (block == null) + { + // EOF + break; + } + + if (block.RecordType == RecordHeaderType.INFO) + { + blocks.Add(block); + break; + } + } + catch (Exception) + { + // fallthrough + return; + } + } + + // process the INFO block + var infoBlock = blocks.Find(static a => a.RecordType == RecordHeaderType.INFO); + + if (infoBlock != null) + { + // platform records consist of an array of 4 byte integers + // this is because an image can potentially run on multiple platforms + // for now, just take the first bizhawk supported platform we find + + bool found = false; + + switch (infoBlock.INFOplatform1) + { + case 1: + IdentifiedSystem = VSystemID.Raw.Amiga; + found = true; + break; + case 4: + IdentifiedSystem = VSystemID.Raw.AmstradCPC; + found = true; + break; + case 5: + IdentifiedSystem = VSystemID.Raw.ZXSpectrum; + found = true; + break; + case 8: + IdentifiedSystem = VSystemID.Raw.C64; + found = true; + break; + + case 2: // Atari ST + case 3: // PC + case 6: // Sam Coupe + case 7: // Archimedes + case 9: // Atari 8-bit + case 0: // None + default: // Unknown + break; + } + + if (found) + { + return; + } + + switch (infoBlock.INFOplatform2) + { + case 1: + IdentifiedSystem = VSystemID.Raw.Amiga; + found = true; + break; + case 4: + IdentifiedSystem = VSystemID.Raw.AmstradCPC; + found = true; + break; + case 5: + IdentifiedSystem = VSystemID.Raw.ZXSpectrum; + found = true; + break; + case 8: + IdentifiedSystem = VSystemID.Raw.C64; + found = true; + break; + + case 2: // Atari ST + case 3: // PC + case 6: // Sam Coupe + case 7: // Archimedes + case 9: // Atari 8-bit + case 0: // None + default: // Unknown + break; + } + + if (found) + { + return; + } + + switch (infoBlock.INFOplatform3) + { + case 1: + IdentifiedSystem = VSystemID.Raw.Amiga; + found = true; + break; + case 4: + IdentifiedSystem = VSystemID.Raw.AmstradCPC; + found = true; + break; + case 5: + IdentifiedSystem = VSystemID.Raw.ZXSpectrum; + found = true; + break; + case 8: + IdentifiedSystem = VSystemID.Raw.C64; + found = true; + break; + + case 2: // Atari ST + case 3: // PC + case 6: // Sam Coupe + case 7: // Archimedes + case 9: // Atari 8-bit + case 0: // None + default: // Unknown + break; + } + + if (found) + { + return; + } + + switch (infoBlock.INFOplatform4) + { + case 1: + IdentifiedSystem = VSystemID.Raw.Amiga; + found = true; + break; + case 4: + IdentifiedSystem = VSystemID.Raw.AmstradCPC; + found = true; + break; + case 5: + IdentifiedSystem = VSystemID.Raw.ZXSpectrum; + found = true; + break; + case 8: + IdentifiedSystem = VSystemID.Raw.C64; + found = true; + break; + + case 2: // Atari ST + case 3: // PC + case 6: // Sam Coupe + case 7: // Archimedes + case 9: // Atari 8-bit + case 0: // None + default: // Unknown + break; + } + } + } + + + + /// + /// Returns an int32 from a byte array based on offset (in BIG ENDIAN format) + /// + public static int GetBEInt32(byte[] buf, int offsetIndex) + { + byte[] b = new byte[4]; + Array.Copy(buf, offsetIndex, b, 0, 4); + byte[] buffer = b.Reverse().ToArray(); + int pos = 0; + return buffer[pos++] | buffer[pos++] << 8 | buffer[pos++] << 16 | buffer[pos++] << 24; + } + + public class IPFBlock + { + public RecordHeaderType RecordType; + public int BlockLength; + public int CRC; + public byte[]? RawBlockData; + public int StartPos; + + public int INFOmediaType; + public int INFOencoderType; + public int INFOencoderRev; + public int INFOfileKey; + public int INFOfileRev; + public int INFOorigin; + public int INFOminTrack; + public int INFOmaxTrack; + public int INFOminSide; + public int INFOmaxSide; + public int INFOcreationDate; + public int INFOcreationTime; + public int INFOplatform1; + public int INFOplatform2; + public int INFOplatform3; + public int INFOplatform4; + public int INFOdiskNumber; + public int INFOcreatorId; + + public int IMGEtrack; + public int IMGEside; + public int IMGEdensity; + public int IMGEsignalType; + public int IMGEtrackBytes; + public int IMGEstartBytePos; + public int IMGEstartBitPos; + public int IMGEdataBits; + public int IMGEgapBits; + public int IMGEtrackBits; + public int IMGEblockCount; + public int IMGEencoderProcess; + public int IMGEtrackFlags; + public int IMGEdataKey; + + public int DATAlength; + public int DATAbitSize; + public int DATAcrc; + public int DATAdataKey; + public byte[]? DATAextraDataRaw; + + public static IPFBlock? ParseNextBlock(ref int startPos, byte[] data, List blockCollection) + { + IPFBlock ipf = new IPFBlock(); + ipf.StartPos = startPos; + + if (startPos >= data.Length) + { + // EOF + return null; + } + + // assume the startPos passed in is actually the start of a new block + // look for record header ident + string ident = Encoding.ASCII.GetString(data, startPos, 4); + startPos += 4; + try + { + ipf.RecordType = (RecordHeaderType) Enum.Parse(typeof(RecordHeaderType), ident); + } + catch + { + ipf.RecordType = RecordHeaderType.None; + } + + // setup for actual block size + ipf.BlockLength = GetBEInt32(data, startPos); startPos += 4; + ipf.CRC = GetBEInt32(data, startPos); startPos += 4; + ipf.RawBlockData = new byte[ipf.BlockLength]; + Array.Copy(data, ipf.StartPos, ipf.RawBlockData, 0, ipf.BlockLength); + + switch (ipf.RecordType) + { + // Nothing to process / unknown + // just move ahead + case RecordHeaderType.CAPS: + case RecordHeaderType.TRCK: + case RecordHeaderType.DUMP: + case RecordHeaderType.CTEI: + case RecordHeaderType.CTEX: + default: + startPos = ipf.StartPos + ipf.BlockLength; + break; + + // INFO block + case RecordHeaderType.INFO: + // INFO header is followed immediately by an INFO block + ipf.INFOmediaType = GetBEInt32(data, startPos); startPos += 4; + ipf.INFOencoderType = GetBEInt32(data, startPos); startPos += 4; + ipf.INFOencoderRev = GetBEInt32(data, startPos); startPos += 4; + ipf.INFOfileKey = GetBEInt32(data, startPos); startPos += 4; + ipf.INFOfileRev = GetBEInt32(data, startPos); startPos += 4; + ipf.INFOorigin = GetBEInt32(data, startPos); startPos += 4; + ipf.INFOminTrack = GetBEInt32(data, startPos); startPos += 4; + ipf.INFOmaxTrack = GetBEInt32(data, startPos); startPos += 4; + ipf.INFOminSide = GetBEInt32(data, startPos); startPos += 4; + ipf.INFOmaxSide = GetBEInt32(data, startPos); startPos += 4; + ipf.INFOcreationDate = GetBEInt32(data, startPos); startPos += 4; + ipf.INFOcreationTime = GetBEInt32(data, startPos); startPos += 4; + ipf.INFOplatform1 = GetBEInt32(data, startPos); startPos += 4; + ipf.INFOplatform2 = GetBEInt32(data, startPos); startPos += 4; + ipf.INFOplatform3 = GetBEInt32(data, startPos); startPos += 4; + ipf.INFOplatform4 = GetBEInt32(data, startPos); startPos += 4; + ipf.INFOdiskNumber = GetBEInt32(data, startPos); startPos += 4; + ipf.INFOcreatorId = GetBEInt32(data, startPos); startPos += 4; + startPos += 12; // reserved + break; + + case RecordHeaderType.IMGE: + ipf.IMGEtrack = GetBEInt32(data, startPos); startPos += 4; + ipf.IMGEside = GetBEInt32(data, startPos); startPos += 4; + ipf.IMGEdensity = GetBEInt32(data, startPos); startPos += 4; + ipf.IMGEsignalType = GetBEInt32(data, startPos); startPos += 4; + ipf.IMGEtrackBytes = GetBEInt32(data, startPos); startPos += 4; + ipf.IMGEstartBytePos = GetBEInt32(data, startPos); startPos += 4; + ipf.IMGEstartBitPos = GetBEInt32(data, startPos); startPos += 4; + ipf.IMGEdataBits = GetBEInt32(data, startPos); startPos += 4; + ipf.IMGEgapBits = GetBEInt32(data, startPos); startPos += 4; + ipf.IMGEtrackBits = GetBEInt32(data, startPos); startPos += 4; + ipf.IMGEblockCount = GetBEInt32(data, startPos); startPos += 4; + ipf.IMGEencoderProcess = GetBEInt32(data, startPos); startPos += 4; + ipf.IMGEtrackFlags = GetBEInt32(data, startPos); startPos += 4; + ipf.IMGEdataKey = GetBEInt32(data, startPos); startPos += 4; + startPos += 12; // reserved + break; + + case RecordHeaderType.DATA: + ipf.DATAlength = GetBEInt32(data, startPos); + if (ipf.DATAlength == 0) + { + ipf.DATAextraDataRaw = Array.Empty(); + ipf.DATAlength = 0; + } + else + { + ipf.DATAextraDataRaw = new byte[ipf.DATAlength]; + } + startPos += 4; + ipf.DATAbitSize = GetBEInt32(data, startPos); startPos += 4; + ipf.DATAcrc = GetBEInt32(data, startPos); startPos += 4; + ipf.DATAdataKey = GetBEInt32(data, startPos); startPos += 4; + + if (ipf.DATAlength != 0) + { + Array.Copy(data, startPos, ipf.DATAextraDataRaw, 0, ipf.DATAlength); + } + + startPos += ipf.DATAlength; + break; + } + + return ipf; + } + } + + public enum RecordHeaderType + { + None, + CAPS, + DUMP, + DATA, + TRCK, + INFO, + IMGE, + CTEI, + CTEX, + } + } +}