From 93c056db49cc99d028a12c515d3c59bfc22629a5 Mon Sep 17 00:00:00 2001 From: Zorchenhimer Date: Sat, 6 Dec 2025 14:15:08 -0500 Subject: [PATCH] Initial commit Bare-bones functionality so far. Lists blocks in a rom with minimal information. --- .gitignore | 2 + Makefile | 11 + blocks.go | 194 ++++++++++++++++ cmd/fdslist.go | 79 +++++++ go.mod | 10 + go.sum | 15 ++ licensees.go | 599 +++++++++++++++++++++++++++++++++++++++++++++++++ reader.go | 216 ++++++++++++++++++ rom.go | 14 ++ 9 files changed, 1140 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 blocks.go create mode 100644 cmd/fdslist.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 licensees.go create mode 100644 reader.go create mode 100644 rom.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d1c6624 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +bin/ +*.fds diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d99053a --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ + +CMDS= \ + bin/fdslist + +all: bin $(CMDS) + +bin: + mkdir bin + +bin/%: cmd/%.go *.go + go build -o $@ $< diff --git a/blocks.go b/blocks.go new file mode 100644 index 0000000..211a609 --- /dev/null +++ b/blocks.go @@ -0,0 +1,194 @@ +package fds + +import ( + "fmt" +) + +type BlockType byte + +const ( + BtInfo BlockType = 0x01 + BtFileAmount BlockType = 0x02 + BtFileHeader BlockType = 0x03 + BtFileData BlockType = 0x04 +) + +func (bt BlockType) String() string { + switch bt { + case BtInfo: + return "BlockInfo" + case BtFileAmount: + return "BlockAmount" + case BtFileHeader: + return "BlockFileHeader" + case BtFileData: + return "BlockFileData" + } + + return "Unknown block" +} + +type DiskBlock interface { + Type() BlockType + String() string +} + +// Block 1 +type BlockInfo struct { + // always 0x01 + BlockCode byte + + // literal ASCII: "*NINTENDO-HVC*" + DiskVerification [14]byte + Licensee uint8 + + // 3 byte ASCII code + GameName [3]byte + + // 0x20: " " normal disk + // 0x45: "E" Event + // 0x4A: "J" Unknown + // 0x52: "R" Reduction in price via advert (lmao what?) + GameType uint8 + + GameVersion uint8 + SideNumber uint8 + DiskNumber uint8 + + // 0x01 for FMC blue-disk + // 0x00 otherwise + DiskType uint8 + + Unknown_18 uint8 // unknown; 0x00 in all known games + + BoodReadFileCode uint8 + + Unknown_1A [5]byte // unknown; 0xFF x5 + + // BCD YMD, but 1-indexed from Japanese calendar era (fuck you, lmao). + ManufacturingDate [3]byte + + CountryCode uint8 // 0x49 = japan + Unknown_23 uint8 // region code? + Unknown_24 uint8 // location/site? + Unknown_25 [2]byte // raw: 0x00 0x22 + Unknown_27 [5]byte // game info representation? + + // BCD YMD (same as above). Disk Writer kiosk write date? + RewrittenDate [3]byte + + Unknown_2F uint8 + Unknown_30 uint8 // raw: 0x80 + + DiskWriterSerial uint16 + + Unknown_33 uint8 // raw: 0x07 + + // BCD format. 0x00 == original + RewriteCount byte + + // 0x00 = SideA; 0x01 = SideB + ActualDiskSide uint8 + + // 0x00 yellow + // 0xFF blue + // 0xFE prototype/internal (white or pink) + DiskTypeOther uint8 + + DiskVersion uint8 + + // CRC-16/KERMIT. apparently this will work: github.com/sigurn/crc16 + CRC uint16 // omitted from FDS files (fucking whyyyyyyy) +} + +func (b *BlockInfo) Type() BlockType { + return BtInfo +} + +func (b *BlockInfo) String() string { + return fmt.Sprintf("FdsBlockInfo Licensee:%02X GameName:%q GameType:%c", + b.Licensee, + b.GameName, + b.GameType, + ) +} + +const Template_FdsBlockInfo string = `Licensee: {{.Licensee}} + {{.LicEn}} + {{.LicJp}} +Name: {{.GameName}} Version: {{.GameVersion}} Type: {{.GameType}}` + +func (b *BlockInfo) Info() string { + return "yup" +} + +//func (b *BlockInfo) Info() string { +// return fmt.Sprintf("Licensee:%02X\n %s\n %s\nGameName: %s", +// b.Licensee, +// LicenseeEnName(b.Licensee), +// LicenseeJpName(b.Licensee), +// ) +//} + +type BlockFileAmount struct { + BlockCode byte // raw: 0x02 + FileAmount uint8 + CRC uint16 // omitted from FDS files (fucking whyyyyyyy) +} + +func (b *BlockFileAmount) Type() BlockType { + return BtFileAmount +} + +func (b *BlockFileAmount) String() string { + return fmt.Sprintf("BlockFileAmount FileAmount:%d", b.FileAmount) +} + +type BlockFileHeader struct { + BlockCode byte // raw: 0x03 + + FileNumber uint8 + FileId uint8 + FileName [8]byte + FileAddress uint16 // destination when loading + FileSize uint16 + + // 0x00: Program (PRAM) + // 0x01: Character (CRAM) + // 0x02: Nametable (VRAM) + FileType uint8 + + CRC uint16 // omitted from FDS files (fucking whyyyyyyy) +} + +func (b *BlockFileHeader) Type() BlockType { + return BtFileHeader +} + +func (b *BlockFileHeader) String() string { + return fmt.Sprintf("BlockFileHeader FileNumber:%d FileId:%d FileName:%q FileAddress:$%04X FileSize:%d FileType:%d", + b.FileNumber, + b.FileId, + string(b.FileName[:]), + b.FileAddress, + b.FileSize, + b.FileType, + ) +} + +type BlockFileData struct { + BlockCode byte // raw: 0x04 + + // Disk data (FdsFileHeader.FileSize length) + Data []byte + + CRC uint16 // omitted from FDS files (fucking whyyyyyyy) +} + +func (b *BlockFileData) Type() BlockType { + return BtFileData +} + +func (b *BlockFileData) String() string { + return "BlockFileData" +} diff --git a/cmd/fdslist.go b/cmd/fdslist.go new file mode 100644 index 0000000..c68d198 --- /dev/null +++ b/cmd/fdslist.go @@ -0,0 +1,79 @@ +package main + +import ( + "fmt" + "os" + //"encoding/binary" + //"bufio" + //"bytes" + //"io" + "path/filepath" + + "github.com/alexflint/go-arg" + + "git.zorchenhimer.com/go-fds" +) + +type Arguments struct { + Input string `arg:"positional,required"` +} + + +func main() { + args := &Arguments{} + arg.MustParse(args) + + err := run(args) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +func run(args *Arguments) error { + file, err := os.Open(args.Input) + if err != nil { + return err + } + defer file.Close() + + //reader := bufio.NewReader(file) + //magic, err := reader.Peek(4) + //if err != nil { + // return fmt.Errorf("Unable to peek header: %w", err) + //} + + //header := &fds.FdsHeader{} + + //// "FDS" + EOF. header may be omitted. + //if bytes.Equal(magic, []byte{0x46, 0x44, 0x53, 0x1A}) { + // err = binary.Read(reader, binary.LittleEndian, header) + // if err != nil { + // return fmt.Errorf("Error reading header: %w", err) + // } + //} + + //if header != nil { + // fmt.Println("Header found. sides:", header.Sides) + //} else { + // fmt.Println("No header") + //} + + //blocks, err := fds.ReadBlocks(reader, true) + //if err != nil { + // return err + //} + + rom, err := fds.ReadRom(file, filepath.Ext(args.Input) == ".fds") + if err != nil { + return err + } + + for _, block := range rom.Blocks { + fmt.Println(block) + } + + return nil +} + + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9c46265 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module git.zorchenhimer.com/go-fds + +go 1.25.4 + +require ( + github.com/alexflint/go-arg v1.6.0 + github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 +) + +require github.com/alexflint/go-scalar v1.2.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1c9a2a0 --- /dev/null +++ b/go.sum @@ -0,0 +1,15 @@ +github.com/alexflint/go-arg v1.6.0 h1:wPP9TwTPO54fUVQl4nZoxbFfKCcy5E6HBCumj1XVRSo= +github.com/alexflint/go-arg v1.6.0/go.mod h1:A7vTJzvjoaSTypg4biM5uYNTkJ27SkNTArtYXnlqVO8= +github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw= +github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 h1:NVK+OqnavpyFmUiKfUMHrpvbCi2VFoWTrcpI7aDaJ2I= +github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/licensees.go b/licensees.go new file mode 100644 index 0000000..7853968 --- /dev/null +++ b/licensees.go @@ -0,0 +1,599 @@ +package fds + +func LicenseeEnName(id byte) string { + lic, ok := Licensees[id] + if !ok { + return "????" + } + + return lic.English +} + +func LicenseeJpName(id byte) string { + lic, ok := Licensees[id] + if !ok { + return "????" + } + + return lic.Japanese +} + +type Licensee struct { + Id byte + English string + Japanese string +} + +var Licensees = map[byte]Licensee{ + 0x00: Licensee{ Id:0x00, + English:"", + Japanese:"<非公認>" }, + + 0x01: Licensee{ Id:0x01, + English:"Nintendo", + Japanese:"任天堂" }, + + 0x07: Licensee{ Id:0x07, + English:"Nomura Securities? (unverified)", + Japanese:"野村證券? (未確認)" }, + + 0x08: Licensee{ Id:0x08, + English:"Capcom", + Japanese:"カプコン" }, + + 0x09: Licensee{ Id:0x09, + English:"Hot-B", + Japanese:"ホット・ビィ" }, + + 0x0A: Licensee{ Id:0x0A, + English:"Jaleco", + Japanese:"ジャレコ" }, + + 0x0B: Licensee{ Id:0x0B, + English:"Coconuts Japan Entertainment", + Japanese:"ココナッツジャパンエンターテイメント" }, + + 0x13: Licensee{ Id:0x13, + English:"Electronic Arts (Japan)", + Japanese:"エレクトロニック・アーツ (日本)" }, + + 0x18: Licensee{ Id:0x18, + English:"Hudson Soft", + Japanese:"ハドソン" }, + + 0x21: Licensee{ Id:0x21, + English:"Tokai Engineering", + Japanese:"東海エンジニアリング" }, + + 0x28: Licensee{ Id:0x28, + English:"Kemco (Japan)", + Japanese:"ケムコ (日本)" }, + + 0x29: Licensee{ Id:0x29, + English:"SETA (Japan)", + Japanese:"セタ (日本)" }, + + 0x2B: Licensee{ Id:0x2B, + English:"Tamtex", + Japanese:"タムテックス" }, + + 0x35: Licensee{ Id:0x35, + English:"Hector Playing Interface (Hect)", + Japanese:"ヘクト" }, + + 0x3D: Licensee{ Id:0x3D, + English:"Loriciel", + Japanese:"ロリシエル" }, + + 0x3E: Licensee{ Id:0x3E, + English:"Gremlin", + Japanese:"グレムリン" }, + + 0x40: Licensee{ Id:0x40, + English:"Seika Corporation", + Japanese:"セイカ" }, + + 0x41: Licensee{ Id:0x41, + English:"Ubisoft", + Japanese:"ウビソフト" }, + + 0x46: Licensee{ Id:0x46, + English:"System 3", + Japanese:"システム 3" }, + + 0x49: Licensee{ Id:0x49, + English:"Irem", + Japanese:"アイレム" }, + + 0x4A: Licensee{ Id:0x4A, + English:"Gakken", + Japanese:"学習研究社" }, + + 0x50: Licensee{ Id:0x50, + English:"Absolute", + Japanese:"アブソールト" }, + + 0x51: Licensee{ Id:0x51, + English:"Acclaim (NA)", + Japanese:"アクレイム (北米)" }, + + 0x52: Licensee{ Id:0x52, + English:"Activision", + Japanese:"アクティビシォン" }, + + 0x53: Licensee{ Id:0x53, + English:"American Sammy", + Japanese:"アメリカン サミー" }, + + 0x54: Licensee{ Id:0x54, + English:"GameTek", + Japanese:"ガメテック" }, + + 0x55: Licensee{ Id:0x55, + English:"Hi Tech Expressions", + Japanese:"ハイ テック エクスープレシオンス" }, + + 0x56: Licensee{ Id:0x56, + English:"LJN", + Japanese:"LJN" }, + + 0x57: Licensee{ Id:0x57, + English:"Matchbox Toys", + Japanese:"マッチョボックス トイズ" }, + + 0x58: Licensee{ Id:0x58, + English:"Mattel", + Japanese:"マッテル" }, + + 0x59: Licensee{ Id:0x59, + English:"Milton Bradley", + Japanese:"ミルトン ブラッドリー" }, + + 0x5A: Licensee{ Id:0x5A, + English:"Mindscape / Software Toolworks", + Japanese:"マインドスケップ / ソフトワー トゥールウアクス" }, + + 0x5B: Licensee{ Id:0x5B, + English:"SETA (NA)", + Japanese:"セタ (北米)" }, + + 0x5C: Licensee{ Id:0x5C, + English:"Taxan", + Japanese:"タクサン" }, + + 0x5D: Licensee{ Id:0x5D, + English:"Tradewest", + Japanese:"トレードワェスト" }, + + 0x5E: Licensee{ Id:0x5E, + English:"INTV Corporation", + Japanese:"INTV" }, + + 0x60: Licensee{ Id:0x60, + English:"Titus", + Japanese:"タイタス" }, + + 0x61: Licensee{ Id:0x61, + English:"Virgin Games", + Japanese:"ヴァージン" }, + + 0x67: Licensee{ Id:0x67, + English:"Ocean", + Japanese:"オシャン" }, + + 0x69: Licensee{ Id:0x69, + English:"Electronic Arts (NA)", + Japanese:"エレクトロニック・アーツ (北米)" }, + + 0x6B: Licensee{ Id:0x6B, + English:"Beam Software", + Japanese:"ビーム" }, + + 0x6E: Licensee{ Id:0x6E, + English:"Elite Systems", + Japanese:"エリット" }, + + 0x6F: Licensee{ Id:0x6F, + English:"Electro Brain", + Japanese:"エレクトロ ブレイン" }, + + 0x70: Licensee{ Id:0x70, + English:"Infogrames", + Japanese:"インフォグレムス" }, + + 0x72: Licensee{ Id:0x72, + English:"JVC", + Japanese:"JVC" }, + + 0x73: Licensee{ Id:0x73, + English:"Parker Brothers", + Japanese:"パーカー ブロザーズ" }, + + 0x75: Licensee{ Id:0x75, + English:"The Sales Curve / SCi", + Japanese:"ザ・セルズ・カーヴ / SCi" }, + + 0x78: Licensee{ Id:0x78, + English:"THQ", + Japanese:"THQ" }, + + 0x79: Licensee{ Id:0x79, + English:"Accolade", + Japanese:"アッコレド" }, + + 0x7A: Licensee{ Id:0x7A, + English:"Triffix", + Japanese:"トリフィクス" }, + + 0x7C: Licensee{ Id:0x7C, + English:"Microprose Software", + Japanese:"マイクロプロス" }, + + 0x7F: Licensee{ Id:0x7F, + English:"Kemco (NA)", + Japanese:"ケムコ (北米)" }, + + 0x80: Licensee{ Id:0x80, + English:"Misawa Entertainment", + Japanese:"ミサワエンターテインメント" }, + + 0x83: Licensee{ Id:0x83, + English:"G. Amusements Co.", + Japanese:"ジー・アミューズメント" }, + + 0x85: Licensee{ Id:0x85, + English:"G.O 1", + Japanese:"ジー・オー・ワン" }, + + 0x86: Licensee{ Id:0x86, + English:"Tokuma Shoten Intermedia", + Japanese:"徳間書店インターメディア" }, + + 0x89: Licensee{ Id:0x89, + English:"Nihon Maicom Kaihatsu (NMK)", + Japanese:"NMK" }, + + 0x8B: Licensee{ Id:0x8B, + English:"BulletProof Software (BPS)", + Japanese:"BPS" }, + + 0x8C: Licensee{ Id:0x8C, + English:"VIC Tokai", + Japanese:"ビック東海" }, + + 0x8D: Licensee{ Id:0x8D, + English:"Sanritsu", + Japanese:"サンリツ" }, + + 0x8E: Licensee{ Id:0x8E, + English:"Character Soft", + Japanese:"キャラクターソフト" }, + + 0x8F: Licensee{ Id:0x8F, + English:"I'Max", + Japanese:"アイマックス" }, + + 0x94: Licensee{ Id:0x94, + English:"Toaplan", + Japanese:"株式会社東亜プラン" }, + + 0x95: Licensee{ Id:0x95, + English:"Varie", + Japanese:"バリエ" }, + + 0x96: Licensee{ Id:0x96, + English:"Yonezawa Party Room 21 / S'Pal", + Japanese:"ヨネザワPR21 / エスパル" }, + + 0x99: Licensee{ Id:0x99, + English:"Pack-In-Video", + Japanese:"パックインビデオ" }, + + 0x9A: Licensee{ Id:0x9A, + English:"Nihon Bussan", + Japanese:"日本物産" }, + + 0x9B: Licensee{ Id:0x9B, + English:"Tecmo", + Japanese:"テクモ" }, + + 0x9C: Licensee{ Id:0x9C, + English:"Imagineer", + Japanese:"イマジニア" }, + + 0x9E: Licensee{ Id:0x9E, + English:"Face", + Japanese:"フェイス" }, + + 0xA2: Licensee{ Id:0xA2, + English:"Scorpion Soft", + Japanese:"スコーピオンソフト" }, + + 0xA3: Licensee{ Id:0xA3, + English:"Broderbund", + Japanese:"ブロダーブンド" }, + + 0xA4: Licensee{ Id:0xA4, + English:"Konami", + Japanese:"コナミ" }, + + 0xA5: Licensee{ Id:0xA5, + English:"K. Amusement Leasing Co. (KAC)", + Japanese:"ケイ・アミューズメントリース" }, + + 0xA6: Licensee{ Id:0xA6, + English:"Kawada Co., Ltd.", + Japanese:"河田" }, + + 0xA7: Licensee{ Id:0xA7, + English:"Takara", + Japanese:"タカラ" }, + + 0xA8: Licensee{ Id:0xA8, + English:"Royal Industries", + Japanese:"ロイヤル工業" }, + + 0xA9: Licensee{ Id:0xA9, + English:"Tecnos", + Japanese:"テクノス" }, + + 0xAA: Licensee{ Id:0xAA, + English:"Victor Musical Industries", + Japanese:"ビクター音楽産業" }, + + 0xAB: Licensee{ Id:0xAB, + English:"Hi-Score Media Work", + Japanese:"ハイスコアメディアワーク" }, + + 0xAC: Licensee{ Id:0xAC, + English:"Toei Animation", + Japanese:"東映動画" }, + + 0xAD: Licensee{ Id:0xAD, + English:"Toho (Japan)", + Japanese:"東宝 (日本)" }, + + 0xAE: Licensee{ Id:0xAE, + English:"TSS", + Japanese:"ティーエスエス" }, + + 0xAF: Licensee{ Id:0xAF, + English:"Namco", + Japanese:"ナムコ" }, + + 0xB0: Licensee{ Id:0xB0, + English:"Acclaim (Japan)", + Japanese:"アクレイム" }, + + 0xB1: Licensee{ Id:0xB1, + English:"ASCII Corporation / Nexoft", + Japanese:"アスキー / Nexoft" }, + + 0xB2: Licensee{ Id:0xB2, + English:"Bandai", + Japanese:"バンダイ" }, + + 0xB3: Licensee{ Id:0xB3, + English:"Soft Pro Inc.", + Japanese:"ソフトプロ" }, + + 0xB4: Licensee{ Id:0xB4, + English:"Enix", + Japanese:"エニックス" }, + + 0xB5: Licensee{ Id:0xB5, + English:"dB-SOFT", + Japanese:"デービーソフト" }, + + 0xB6: Licensee{ Id:0xB6, + English:"HAL Laboratory", + Japanese:"HAL研究所" }, + + 0xB7: Licensee{ Id:0xB7, + English:"SNK", + Japanese:"SNK" }, + + 0xB9: Licensee{ Id:0xB9, + English:"Pony Canyon", + Japanese:"ポニーキャニオン" }, + + 0xBA: Licensee{ Id:0xBA, + English:"Culture Brain", + Japanese:"カルチャーブレーン" }, + + 0xBB: Licensee{ Id:0xBB, + English:"Sunsoft", + Japanese:"サンソフト" }, + + 0xBC: Licensee{ Id:0xBC, + English:"Toshiba EMI", + Japanese:"東芝EMI" }, + + 0xBD: Licensee{ Id:0xBD, + English:"CBS/Sony Group", + Japanese:"CBS・ソニー" }, + + 0xBF: Licensee{ Id:0xBF, + English:"Sammy Corporation", + Japanese:"サミー" }, + + 0xC0: Licensee{ Id:0xC0, + English:"Taito", + Japanese:"タイトー" }, + + 0xC1: Licensee{ Id:0xC1, + English:"Sunsoft / Ask Co., Ltd.", + Japanese:"サンソフト / アスク講談社" }, + + 0xC2: Licensee{ Id:0xC2, + English:"Kemco", + Japanese:"ケムコ" }, + + 0xC3: Licensee{ Id:0xC3, + English:"Square / Disk Original Group (DOG)", + Japanese:"スクウェア / ディスク・オリジナル・グループ" }, + + 0xC4: Licensee{ Id:0xC4, + English:"Tokuma Shoten", + Japanese:"徳間書店" }, + + 0xC5: Licensee{ Id:0xC5, + English:"Data East", + Japanese:"データイースト" }, + + 0xC6: Licensee{ Id:0xC6, + English:"Tonkin House / Tokyo Shoseki", + Japanese:"トンキンハウス / 東京書籍" }, + + 0xC7: Licensee{ Id:0xC7, + English:"East Cube / Toho (NA)", + Japanese:"イーストキューブ / 東宝 (北米)" }, + + 0xC8: Licensee{ Id:0xC8, + English:"Koei", + Japanese:"コーエー" }, + + 0xC9: Licensee{ Id:0xC9, + English:"UPL", + Japanese:"ユーピーエル" }, + + 0xCA: Licensee{ Id:0xCA, + English:"Konami / Ultra / Palcom", + Japanese:"コナミ" }, + + 0xCB: Licensee{ Id:0xCB, + English:"NTVIC / VAP", + Japanese:"バップ" }, + + 0xCC: Licensee{ Id:0xCC, + English:"Use Co., Ltd.", + Japanese:"ユース" }, + + 0xCD: Licensee{ Id:0xCD, + English:"Meldac", + Japanese:"メルダック" }, + + 0xCE: Licensee{ Id:0xCE, + English:"Pony Canyon / FCI", + Japanese:"ポニーキャニオン" }, + + 0xCF: Licensee{ Id:0xCF, + English:"Angel", + Japanese:"エンジェル" }, + + 0xD0: Licensee{ Id:0xD0, + English:"Disco", + Japanese:"ディスコ" }, + + 0xD1: Licensee{ Id:0xD1, + English:"Sofel", + Japanese:"ソフエル" }, + + 0xD2: Licensee{ Id:0xD2, + English:"Bothtec, Inc. / Quest", + Japanese:"ボーステック / クエスト" }, + + 0xD3: Licensee{ Id:0xD3, + English:"Sigma Enterprises", + Japanese:"シグマ商事" }, + + 0xD4: Licensee{ Id:0xD4, + English:"Ask Corp.", + Japanese:"アスク講談社" }, + + 0xD5: Licensee{ Id:0xD5, + English:"Kyugo Trading Co.", + Japanese:"九娯貿易" }, + + 0xD6: Licensee{ Id:0xD6, + English:"Naxat Soft / Kaga Tech", + Japanese:"ナグザットソフト / 加賀テック" }, + + 0xD8: Licensee{ Id:0xD8, + English:"Status", + Japanese:"ステイタス" }, + + 0xD9: Licensee{ Id:0xD9, + English:"Banpresto", + Japanese:"バンプレスト" }, + + 0xDA: Licensee{ Id:0xDA, + English:"Tomy", + Japanese:"トミー" }, + + 0xDB: Licensee{ Id:0xDB, + English:"Hiro Co., Ltd.", + Japanese:"ヒロ" }, + + 0xDD: Licensee{ Id:0xDD, + English:"Nippon Computer Systems (NCS) / Masaya Games", + Japanese:"日本コンピュータシステム / メサイヤゲームス" }, + + 0xDE: Licensee{ Id:0xDE, + English:"Human Creative", + Japanese:"ヒューマン" }, + + 0xDF: Licensee{ Id:0xDF, + English:"Altron", + Japanese:"アルトロン" }, + + 0xE0: Licensee{ Id:0xE0, + English:"K.K. DCE", + Japanese:"K.K. DCE" }, + + 0xE1: Licensee{ Id:0xE1, + English:"Towa Chiki", + Japanese:"トーワチキ" }, + + 0xE2: Licensee{ Id:0xE2, + English:"Yutaka", + Japanese:"ユタカ" }, + + 0xE3: Licensee{ Id:0xE3, + English:"Kaken Corporation", + Japanese:"科学技研株式会社" }, + + 0xE5: Licensee{ Id:0xE5, + English:"Epoch", + Japanese:"エポック" }, + + 0xE7: Licensee{ Id:0xE7, + English:"Athena", + Japanese:"アテナ" }, + + 0xE8: Licensee{ Id:0xE8, + English:"Asmik", + Japanese:"アスミック" }, + + 0xE9: Licensee{ Id:0xE9, + English:"Natsume", + Japanese:"ナツメ" }, + + 0xEA: Licensee{ Id:0xEA, + English:"King Records", + Japanese:"キングレコード" }, + + 0xEB: Licensee{ Id:0xEB, + English:"Atlus", + Japanese:"アトラス" }, + + 0xEC: Licensee{ Id:0xEC, + English:"Sony Music Entertainment", + Japanese:"ソニー・ミュージックエンタテインメント" }, + + 0xED: Licensee{ Id:0xED, + English:"Pixel Corporation", + Japanese:"ピクセル" }, + + 0xEE: Licensee{ Id:0xEE, + English:"Information Global Service (IGS)", + Japanese:"アイ・ジー・エス" }, + + 0xEF: Licensee{ Id:0xEF, + English:"Fujimic", + Japanese:"フジミック" }, + + 0xF0: Licensee{ Id:0xF0, + English:"A-Wave", + Japanese:"エイ・ウェーブ" }, +} diff --git a/reader.go b/reader.go new file mode 100644 index 0000000..ef38909 --- /dev/null +++ b/reader.go @@ -0,0 +1,216 @@ +package fds + +import ( + //"os" + "fmt" + "io" + "bufio" + "bytes" + "encoding/binary" + + "github.com/sigurn/crc16" +) + +func ReadRom(r io.Reader, IsFds bool) (*Rom, error) { + reader := bufio.NewReader(r) + magic, err := reader.Peek(4) + if err != nil { + return nil, fmt.Errorf("Unable to peek header: %w", err) + } + + rom := &Rom{Header: &FdsHeader{}} + + // "FDS" + EOF. header may be omitted. + if bytes.Equal(magic, FdsHeaderMagic) { + err = binary.Read(reader, binary.LittleEndian, rom.Header) + if err != nil { + return nil, fmt.Errorf("Error reading header: %w", err) + } + } + + rom.Blocks, err = ReadBlocks(reader, IsFds) + return rom, err +} + +func ReadBlocks(r io.Reader, IsFds bool) ([]DiskBlock, error) { + blocks := []DiskBlock{} + dataSize := -1 + readBytes := 0 + + reader := bufio.NewReader(r) + + for { + raw, err := reader.Peek(1) + if err != nil { + if err == io.EOF { + break + } + + return blocks, fmt.Errorf("error peeking block type: %w", err) + } + + var readLen int + var block DiskBlock + idByte := raw[0] + + switch idByte { + case 0x01: + block = &BlockInfo{} + case 0x02: + block = &BlockFileAmount{} + case 0x03: + block = &BlockFileHeader{} + case 0x04: + block = &BlockFileData{} + + // variable length + readLen = dataSize+3 + + case 0x00: + reader.Discard(1) + continue + + default: + fmt.Printf("readBytes: %d ($%04X)\n", readBytes, readBytes) + return blocks, fmt.Errorf("Invalid block type: $%02X", idByte) + } + //fmt.Println("") + + if idByte != 0x04 { + readLen = binary.Size(block) + } + + //fmt.Println("readLen:;", readLen) + // FDS doesn't have the two byte CRC in the data + if IsFds { + readLen -=2 + } + + //fmt.Println("readLen::", readLen) + buf := make([]byte, readLen) + n, err := io.ReadFull(reader, buf) + if err != nil { + return blocks, fmt.Errorf("Error reading block id 0x%02X: %w", idByte, err) + } + readBytes += n + + //var sum uint16 + if IsFds { + //sum = crc16.Checksum(buf, crc16.MakeTable(crc16.CRC16_KERMIT)) + //fmt.Printf("checksum: %04X\n", sum) + buf = append(buf, []byte{0x00, 0x00}...) + } + + //fmt.Printf("idByte:$%02X\nreadLen:%d\nlen(buf):%d\nblocksize:%d\n", idByte, readLen, len(buf), binary.Size(block)) + + if idByte != 0x04 { + _, err = binary.Decode(buf, binary.LittleEndian, block) + if err != nil { + return blocks, fmt.Errorf("Error decoding block id 0x%02X: %w", idByte, err) + } + + if idByte == 0x03 { + fh := block.(*BlockFileHeader) + dataSize = int(fh.FileSize) + } else { + dataSize = -1 + } + + } else { + db := block.(*BlockFileData) + db.BlockCode = buf[0] + + if !IsFds { + db.CRC = uint16(buf[len(buf)-2]) | (uint16(buf[len(buf)-2])<<8) + readLen -= 2 + + } else { + db.CRC = crc16.Checksum(buf[:len(buf)-2], crc16.MakeTable(crc16.CRC16_KERMIT)) + } + + db.Data = buf[1:readLen] + //fmt.Println("len(db.Data)", len(db.Data)) + block = db + //fmt.Printf("Block:%#v\n", block) + } + + blocks = append(blocks, block) + } + + return blocks, nil +} + +//func ReadBlocksFds(r io.Reader) ([]FdsBlock, error) { +// reader := bufio.NewReader(r) +// +// var dataSize int +// blocks := []FdsBlock{} +// for { +// typ, err := reader.Peek(1) +// if err != nil { +// if err == io.EOF { +// break +// } else { +// return nil, err +// } +// } +// +// if len(typ) != 1 { +// return nil, fmt.Errorf("typ read length error") +// } +// //fmt.Printf("typ: %v\n", typ) +// +// if typ[0] == 0x00 { +// //fmt.Println("00 'block'") +// break +// } +// +// var block FdsBlock +// switch BlockType(typ[0]) { +// case BlockInfo: +// block = &FdsBlockInfo{} +// case BlockFileAmount: +// block = &FdsFileAmount{} +// case BlockFileHeader: +// block = &FdsFileHeader{} +// case BlockFileData: +// block = &FdsFileData{} +// default: +// return nil, fmt.Errorf("invalid block type: %02X", typ[0]) +// } +// +// if block.Type() != BlockFileData { +// err = binary.Read(reader, binary.LittleEndian, block) +// if err != nil { +// return nil, fmt.Errorf("decode error: %w", err) +// } +// } else { +// fb := block.(*FdsFileData) +// fb.BlockCode = byte(BlockFileData) +// reader.Discard(1) +// +// if dataSize == 0 { +// return nil, fmt.Errorf("FileData without size") +// } +// fmt.Println("dataSize:", dataSize) +// +// fb.Data = make([]byte, dataSize) +// _, err = reader.Read(fb.Data) +// if err != nil { +// return nil, fmt.Errorf("data read err: %w", err) +// } +// dataSize = 0 +// } +// +// switch block.Type() { +// case BlockFileHeader: +// bh := block.(*FdsFileHeader) +// dataSize = int(bh.FileSize) +// } +// +// fmt.Println(block) +// blocks = append(blocks, block) +// } +// +// return blocks, nil +//} diff --git a/rom.go b/rom.go new file mode 100644 index 0000000..977e962 --- /dev/null +++ b/rom.go @@ -0,0 +1,14 @@ +package fds + +type Rom struct { + Header *FdsHeader + Blocks []DiskBlock +} + +type FdsHeader struct { + Magic [4]byte + Sides uint8 + _ [11]byte // padding +} + +var FdsHeaderMagic []byte = []byte{0x46, 0x44, 0x53, 0x1A}