using System; using System.Collections.Generic; using System.Linq; namespace com.clusterrr.Famicom.Containers { /// /// Single FDS disk side: disk info block, file amount block and file blocks /// public class FdsDiskSide { readonly FdsBlockDiskInfo diskInfoBlock; /// /// Disk info block /// public FdsBlockDiskInfo DiskInfoBlock { get => diskInfoBlock; } /// /// Literal ASCII string: *NINTENDO-HVC* /// public string DiskVerification { get => diskInfoBlock.DiskVerification; } /// /// Manufacturer code. $00 = Unlicensed, $01 = Nintendo /// public FdsBlockDiskInfo.Company ManufacturerCode { get => diskInfoBlock.LicenseeCode; set => diskInfoBlock.LicenseeCode = value; } /// /// 3-letter ASCII code per game (e.g. ZEL for The Legend of Zelda) /// public string GameName { get => diskInfoBlock.GameName; set => diskInfoBlock.GameName = value; } /// /// $20 = " " — Normal disk /// $45 = "E" — Event(e.g.Japanese national DiskFax tournaments) /// $52 = "R" — Reduction in price via advertising /// public char GameType { get => diskInfoBlock.GameType; set => diskInfoBlock.GameType = value; } /// /// Game version/revision number. Starts at $00, increments per revision /// public byte GameVersion { get => diskInfoBlock.GameVersion; set => diskInfoBlock.GameVersion = value; } /// /// Side number. Single-sided disks use A /// public FdsBlockDiskInfo.DiskSides DiskSide { get => diskInfoBlock.DiskSide; set => diskInfoBlock.DiskSide = value; } /// /// Disk number. First disk is $00, second is $01, etc. /// public byte DiskNumber { get => diskInfoBlock.DiskNumber; set => diskInfoBlock.DiskNumber = value; } /// /// Disk type. $00 = FMC ("normal card"), $01 = FSC ("card with shutter"). May correlate with FMC and FSC product codes /// public FdsBlockDiskInfo.DiskTypes DiskType { get => diskInfoBlock.DiskType; set => diskInfoBlock.DiskType = value; } /// /// Boot read file code. Refers to the file code/file number to load upon boot/start-up /// public byte BootFile { get => diskInfoBlock.BootFile; set => diskInfoBlock.BootFile = value; } /// /// Manufacturing date /// public DateTime ManufacturingDate { get => diskInfoBlock.ManufacturingDate; set => diskInfoBlock.ManufacturingDate = value; } /// /// Country code. $49 = Japan /// public FdsBlockDiskInfo.Country CountryCode { get => diskInfoBlock.CountryCode; set => diskInfoBlock.CountryCode = value; } /// /// "Rewritten disk" date. It's speculated this refers to the date the disk was formatted and rewritten by something like a Disk Writer kiosk. /// In the case of an original (non-copied) disk, this should be the same as Manufacturing date /// public DateTime RewrittenDate { get => diskInfoBlock.RewrittenDate; set => diskInfoBlock.RewrittenDate = value; } /// /// Disk Writer serial number /// public ushort DiskWriterSerialNumber { get => diskInfoBlock.DiskWriterSerialNumber; set => diskInfoBlock.DiskWriterSerialNumber = value; } /// /// Disk rewrite count. $00 = Original (no copies) /// public byte DiskRewriteCount { get => diskInfoBlock.DiskRewriteCount; set => diskInfoBlock.DiskRewriteCount = value; } /// /// Actual disk side /// public FdsBlockDiskInfo.DiskSides ActualDiskSide { get => diskInfoBlock.ActualDiskSide; set => diskInfoBlock.ActualDiskSide = value; } /// /// Price code /// public byte Price { get => diskInfoBlock.Price; set => diskInfoBlock.Price = value; } readonly FdsBlockFileAmount fileAmountBlock; /// /// Non-hidden file amount /// public byte FileAmount { get => fileAmountBlock.FileAmount; set => fileAmountBlock.FileAmount = value; } readonly IList files; /// /// Files on disk /// public IList Files { get => files; } /// /// Constructor to create empty FdsDiskSide object /// public FdsDiskSide() { diskInfoBlock = new FdsBlockDiskInfo(); fileAmountBlock = new FdsBlockFileAmount(); files = new List(); } /// /// Constructor to create FdsDiskSide object from blocks and files /// /// Disk info block /// File amount block /// Files public FdsDiskSide(FdsBlockDiskInfo diskInfoBlock, FdsBlockFileAmount fileAmountBlock, IEnumerable files) { this.diskInfoBlock = diskInfoBlock; this.fileAmountBlock = fileAmountBlock; this.files = files.ToList(); } /// /// Constructor to create FdsDiskSide object from blocks /// /// public FdsDiskSide(IEnumerable blocks) { this.diskInfoBlock = (FdsBlockDiskInfo)blocks.First(); this.fileAmountBlock = (FdsBlockFileAmount)blocks.Skip(1).First(); files = new List(); var fileBlocks = blocks.Skip(2).ToArray(); for (int i = 0; i < fileBlocks.Length / 2; i++) { files.Add(new FdsDiskFile((FdsBlockFileHeader)fileBlocks[i * 2], (FdsBlockFileData)fileBlocks[i * 2 + 1])); } } /// /// Constructor to create FdsDiskSide object from raw data /// /// public FdsDiskSide(byte[] data) : this() { int pos = 0; this.diskInfoBlock = FdsBlockDiskInfo.FromBytes(data.Take(56).ToArray()); pos += 56; this.fileAmountBlock = FdsBlockFileAmount.FromBytes(data.Skip(pos).Take(2).ToArray()); pos += 2; while (pos < data.Length) { try { var fileHeaderBlock = FdsBlockFileHeader.FromBytes(data.Skip(pos).Take(16).ToArray()); if (!fileHeaderBlock.IsValid) break; pos += 16; var fileDataBlock = FdsBlockFileData.FromBytes(data.Skip(pos).Take(fileHeaderBlock.FileSize + 1).ToArray()); if (!fileDataBlock.IsValid) break; pos += fileHeaderBlock.FileSize + 1; files.Add(new FdsDiskFile(fileHeaderBlock, fileDataBlock)); } catch { // just break on out of range break; } } } /// /// Change file's "file number" fields orderly /// public void FixFileNumbers() { for (var i = 0; i < files.Count; i++) files[i].FileNumber = (byte)i; } /// /// Get FDS blocks /// /// public IEnumerable GetBlocks() { var blocks = new List { diskInfoBlock, fileAmountBlock }; blocks.AddRange(files.SelectMany(f => new IFdsBlock[] { f.HeaderBlock, f.DataBlock })); return blocks; } /// /// Create FdsDiskSide object from raw data /// /// Data /// FdsDiskSide object public static FdsDiskSide FromBytes(byte[] data) { return new FdsDiskSide(data); } /// /// Return raw data /// /// public byte[] ToBytes() { var data = Enumerable.Concat(Enumerable.Concat(diskInfoBlock.ToBytes(), fileAmountBlock.ToBytes()), files.SelectMany(f => f.ToBytes())).ToArray(); return Enumerable.Concat(data, new byte[65500 - data.Count()]).ToArray(); } /// /// String representation /// /// Game name, disk number, side number as string public override string ToString() => $"{GameName} - disk {DiskNumber + 1}, side {DiskSide}"; } }