From 1c334dfb1bd3577f85d333c094786102e18ab6f2 Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Tue, 10 Mar 2026 19:49:12 -0400 Subject: [PATCH] Add untested FDS <-> QD conversion --- .../CrossModel/FDS.Deserializer.cs | 84 +++++++++++ .../CrossModel/FDS.Serializer.cs | 131 ++++++++++++++++++ 2 files changed, 215 insertions(+) create mode 100644 SabreTools.Serialization/CrossModel/FDS.Deserializer.cs create mode 100644 SabreTools.Serialization/CrossModel/FDS.Serializer.cs diff --git a/SabreTools.Serialization/CrossModel/FDS.Deserializer.cs b/SabreTools.Serialization/CrossModel/FDS.Deserializer.cs new file mode 100644 index 00000000..1552bf93 --- /dev/null +++ b/SabreTools.Serialization/CrossModel/FDS.Deserializer.cs @@ -0,0 +1,84 @@ +using System.Collections.Generic; +using SabreTools.Data.Models.NES; +using SabreTools.Serialization.Interfaces; + +namespace SabreTools.Serialization.CrossModel +{ + /// + public partial class FamicomDiskSystem : ICrossModel + { + /// + public bool Debug { get; set; } = false; + + /// + public FDS? Deserialize(QD? obj) + { + // Ignore invalid data + if (obj?.Data is null || obj.Data.Length == 0) + return null; + + // Ignore incomplete data + if ((obj.Data.Length % 65536) != 0) + return null; + + // Determine the number of sides + byte sides = (byte)(obj.Data.Length / 65536); + if (sides < 1) + return null; + + // Create the FDS for output + var fds = new FDS + { + Header = new FDSHeader + { + IdentificationString = Constants.FDSSignatureBytes, + DiskSides = sides, + } + }; + + // Loop over the sides and convert + List converted = []; + for (int i = 0; i < sides; i++) + { + // Convert the data to manipulate + List data = [.. obj.Data]; + + // Delete block 01 checksum + data.RemoveRange(0x38, 2); + + // Delete block 02 checksum + int pos = 0x3A; + data.RemoveRange(pos, 2); + + // Loop while there are more files + while (data[pos] == 3) + { + // Get the filesize + ushort filesize = (ushort)((data[pos + 0x0E] << 8) | data[pos + 0x0D]); + + // Delete block 03 checksum + data.RemoveRange(pos + 0x10, 2); + + // Skip the file data + pos = pos + 0x10 + 1 + filesize; + + // Delete block 04 checksum + data.RemoveRange(pos, 2); + } + + // Ensure the data is correctly sized + if (data.Count > 65500) + data = data.GetRange(0, 65500); + else if (data.Count < 65500) + data.AddRange(new byte[65500 - data.Count]); + + // Add the data to the output + converted.AddRange(data); + } + + // Assign the converted data and return + fds.Data = [.. converted]; + return fds; + } + } +} diff --git a/SabreTools.Serialization/CrossModel/FDS.Serializer.cs b/SabreTools.Serialization/CrossModel/FDS.Serializer.cs new file mode 100644 index 00000000..a03208c6 --- /dev/null +++ b/SabreTools.Serialization/CrossModel/FDS.Serializer.cs @@ -0,0 +1,131 @@ +using System.Collections.Generic; +using SabreTools.Data.Models.NES; +using SabreTools.Serialization.Interfaces; + +namespace SabreTools.Serialization.CrossModel +{ + /// + public partial class FamicomDiskSystem : ICrossModel + { + /// + public QD? Serialize(FDS? obj) + => Serialize(obj, false); + + /// + /// True to insert null checksums on conversion, false otherwise + public QD? Serialize(FDS? obj, bool nul) + { + // Ignore invalid data + if (obj?.Data is null || obj.Data.Length == 0) + return null; + + // Ignore incomplete data + if ((obj.Data.Length % 65500) != 0) + return null; + + // Determine the number of sides + byte sides = obj.Header != null + ? obj.Header.DiskSides + : (byte)(obj.Data.Length / 65500); + if (sides < 1) + return null; + + // Create the QD for output + var qd = new QD(); + + // Loop over the sides and convert + List converted = []; + for (int i = 0; i < sides; i++) + { + // Convert the data to manipulate + List data = [.. obj.Data]; + + // If the data is unexpected + if (data[0] != 0x01) + break; + + // Insert block 01 checksum + InsertCrc(data, 0, 0x38, nul); + + // Insert block 02 checksum + int pos = 0x3A; + InsertCrc(data, pos, pos + 2, nul); + + // Loop while there are more files + pos = 0x3E; + while (data[pos] == 3) + { + // Get the filesize + ushort filesize = (ushort)((data[pos + 0x0E] << 8) | data[pos + 0x0D]); + + // Insert block 03 checksum + InsertCrc(data, pos, pos + 0x10, nul); + + // Skip the file data + pos += 0x10 + 2; + + // Insert block 04 checksum + InsertCrc(data, pos, pos + 1 + filesize, nul); + pos += 1 + filesize + 2; + } + + // Ensure the data is correctly sized + if (data.Count > 65536) + data = data.GetRange(0, 65536); + else if (data.Count < 65536) + data.AddRange(new byte[65536 - data.Count]); + + // Add the data to the output + converted.AddRange(data); + } + + // Assign the converted data and return + qd.Data = [.. converted]; + return qd; + } + + private void InsertCrc(List data, int start, int end, bool nul) + { + if (!nul) + { + byte[] temp = new byte[end - start]; + data.CopyTo(start, temp, 0, temp.Length); + ushort crc = FdsCrc(temp); + data.Insert(end + 0, (byte)(crc & 0xFF)); + data.Insert(end + 1, (byte)(crc >> 0x08)); + } + else + { + data.Insert(end + 0, 0); + data.Insert(end + 1, 0); + } + } + + // TODO: Replace with CRC-16/KERMIT from Hashing + private ushort FdsCrc(byte[] data) + { + // http://forums.nesdev.com/viewtopic.php?p=194867 + // Do not include any existing checksum, not even the blank checksums 00 00 or FF FF. + // The formula will automatically count 2 0x00 bytes without the programmer adding them manually. + // Also, do not include the gap terminator (0x80) in the data. + // If you wish to do so, change sum to 0x0000. + ushort s = 0x8000; + byte[] padded = [.. data, 0x00, 0x00]; + foreach (byte b in padded) + { + s |= (ushort)(b << 16); + for (int i = 0; i < 8; i++) + { + if ((s & 1) != 0) + { + unchecked { s ^= (ushort)(0x8408 << 1); } + } + + s >>= 1; + } + } + + return s; + } + } +}