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, + } + } +}