package fds import ( "strings" "fmt" "github.com/sigurn/crc16" ) type Rom struct { Name string // filename? Sides []*RomSide } func (rom *Rom) Info() string { builder := &strings.Builder{} fmt.Fprintf(builder, "Name: %s", rom.Name) //sides := []string{} for _, side := range rom.Sides { builder.WriteString("\n\n") builder.WriteString(side.Info()) } return builder.String() } // In the event that a disk has two headers on one physical side of disk, // split the side into two. type RomSide struct { Header *RomHeader FileCount int // more than this number are "hidden" files Files []*RomFile } func (side *RomSide) Info() string { builder := &strings.Builder{} fmt.Fprintln(builder, side.Header.Info()) fmt.Fprintf(builder, "FileCount: listed:%d total:%d\n", side.FileCount, len(side.Files)) sideAB := "?" if side.Header.PhysicalSide == 0x00 { sideAB = "A" } else if side.Header.PhysicalSide == 0x01 { sideAB = "B" } fmt.Fprintf(builder, "Side%s\n", sideAB) files := []string{} for _, file := range side.Files { files = append(files, file.Info()) } builder.WriteString(strings.Join(files, "\n")) return builder.String() } type RomHeader struct { Licensee uint8 GameName string GameType uint8 GameVersion int SideNumber int DiskNumber int DiskType uint8 BootFileId int ManufacturingDate *FdsDate RewrittenDate *FdsDate CountryCode uint8 // 0x49 = japan DiskWriterSerial uint16 RewriteCount int PhysicalSide uint8 DiskTypeOther uint8 DiskVersion uint8 } func (header *RomHeader) Info() string { return fmt.Sprintf("Name: %3s Licensee: $%02X - %s\nMFG Date: %s", header.GameName, header.Licensee, LicenseeEnName(header.Licensee), header.ManufacturingDate, ) } func RomFromBlocks(name string, blocks []DiskBlock) (*Rom, error) { rom := &Rom{ Name: name, Sides: []*RomSide{}, } var side *RomSide var file *RomFile for _, block := range blocks { switch block.Type() { case BtInfo: if file != nil { return nil, fmt.Errorf("Expected BlockFileData, found BlockInfo") } blk := block.(*BlockInfo) side = &RomSide{ Header: &RomHeader{ Licensee: blk.Licensee, GameName: string(blk.GameName[:]), GameType: blk.GameType, GameVersion: int(blk.GameVersion), SideNumber: int(blk.SideNumber), DiskNumber: int(blk.DiskNumber), DiskType: blk.DiskType, BootFileId: int(blk.BootReadFileCode), ManufacturingDate: &FdsDate{ Year: bcdToDec(blk.ManufacturingDate[0]), Month: bcdToDec(blk.ManufacturingDate[1]), Day: bcdToDec(blk.ManufacturingDate[2]), }, RewrittenDate: &FdsDate{ Year: bcdToDec(blk.RewrittenDate[0]), Month: bcdToDec(blk.RewrittenDate[1]), Day: bcdToDec(blk.RewrittenDate[2]), }, CountryCode: blk.CountryCode, DiskWriterSerial: blk.DiskWriterSerial, RewriteCount: bcdToDec(blk.RewriteCount), PhysicalSide: blk.ActualDiskSide, DiskTypeOther: blk.DiskTypeOther, DiskVersion: blk.DiskVersion, }, Files: []*RomFile{}, } rom.Sides = append(rom.Sides, side) case BtFileAmount: if side == nil { return nil, fmt.Errorf("Expected BlockInfo, found BlockFileAmount") } side.FileCount = int((block.(*BlockFileAmount)).FileAmount) case BtFileHeader: if side == nil { return nil, fmt.Errorf("Expected BlockInfo, found BlockFileHeader") } if file != nil { return nil, fmt.Errorf("Expected BlockFileData, found BlockFileHeader") } blk := block.(*BlockFileHeader) file = &RomFile{ Name: BytesToAscii(blk.FileName[:]), Number: int(blk.FileNumber), Id: int(blk.FileId), Type: RomFileType(blk.FileType), Size: int(blk.FileSize), Address: blk.FileAddress, Hidden: (int(blk.FileNumber) >= side.FileCount), } side.Files = append(side.Files, file) case BtFileData: if side == nil { return nil, fmt.Errorf("Expected BlockInfo, found BlockFileData") } if file == nil { return nil, fmt.Errorf("Expected BlockFileHeader, found BlockFileData") } blk := block.(*BlockFileData) file.Data = blk.Data file.CRC = blk.CRC file = nil } } return rom, nil } type RomFileType int const ( FT_Program RomFileType = iota FT_Character FT_Nametable ) func (ft RomFileType) String() string { switch ft { case FT_Program: return "FT_Program" case FT_Character: return "FT_Character" case FT_Nametable: return "FT_Nametable" } return "UNKNOWN" } func (ft RomFileType) Str() string { switch ft { case FT_Program: return "PRG" case FT_Character: return "CHR" case FT_Nametable: return "NT " } return "UNK" } type RomFile struct { Name string Number int Id int Type RomFileType Size int Address uint16 Hidden bool Data []byte CRC uint16 } func (file *RomFile) Info() string { crcStatus := "PASS" // block type is included in the CRC crc := crc16.Checksum(append([]byte{0x04}, file.Data...), crc16.MakeTable(crc16.CRC16_KERMIT)) if crc != file.CRC { crcStatus = fmt.Sprintf("FAIL [$%04X]", crc) } return fmt.Sprintf("%02d $%02X %q %s %5d $%04X-$%04X Hidden:%t CRC:$%04X (%s)", file.Number, file.Id, file.Name, file.Type.Str(), file.Size, file.Address, file.Address+uint16(file.Size), file.Hidden, file.CRC, crcStatus, ) } type FdsHeader struct { Magic [4]byte Sides uint8 _ [11]byte // padding } var FdsHeaderMagic []byte = []byte{0x46, 0x44, 0x53, 0x1A} func BytesToAscii(input []byte) string { builder := &strings.Builder{} for _, b := range input { if b >= ' ' && b <= '~' { builder.WriteByte(b) } else { builder.WriteString(fmt.Sprintf("\\x%02X", b)) } } return builder.String() }