Add Atari 7800 cart support

This commit is contained in:
Matt Nadareski
2026-03-07 21:27:11 -05:00
parent f75bc21d54
commit e2581fc423
21 changed files with 1550 additions and 11 deletions

View File

@@ -150,6 +150,11 @@ namespace ExtractionTool.Features
sz.Extract(OutputPath, Debug);
break;
// Atari 78800 Cart
case Atari7800Cart a7800:
a7800.Extract(OutputPath, Debug);
break;
// BFPK archive
case BFPK bfpk:
bfpk.Extract(OutputPath, Debug);

View File

@@ -158,8 +158,8 @@ namespace InfoPrint.Features
{
using Stream stream = File.Open(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
// Read the first 8 bytes
byte[] magic = stream.PeekBytes(8);
// Read the first 16 bytes
byte[] magic = stream.PeekBytes(16);
// Get the file type
string extension = Path.GetExtension(file).TrimStart('.');

View File

@@ -53,6 +53,7 @@ Options:
| Format Name | Notes |
| --- | --- |
| 7-zip archive | .NET Framework 4.6.2 and greater |
| Atari 7800 (A78) Cart Image | Header and ROM data |
| BFPK custom archive format | |
| bzip2 archive | .NET Framework 4.6.2 and greater |
| Compound File Binary (CFB) | Only CFB common pieces extractable |

View File

@@ -0,0 +1,73 @@
using System.IO;
using System.Linq;
using SabreTools.Serialization.Readers;
using Xunit;
namespace SabreTools.Serialization.Test.Readers
{
public class Atari7800CartTests
{
[Fact]
public void NullArray_Null()
{
byte[]? data = null;
int offset = 0;
var deserializer = new Atari7800Cart();
var actual = deserializer.Deserialize(data, offset);
Assert.Null(actual);
}
[Fact]
public void EmptyArray_Null()
{
byte[]? data = [];
int offset = 0;
var deserializer = new Atari7800Cart();
var actual = deserializer.Deserialize(data, offset);
Assert.Null(actual);
}
[Fact]
public void InvalidArray_Null()
{
byte[]? data = [.. Enumerable.Repeat<byte>(0xFF, 1024)];
int offset = 0;
var deserializer = new Atari7800Cart();
var actual = deserializer.Deserialize(data, offset);
Assert.Null(actual);
}
[Fact]
public void NullStream_Null()
{
Stream? data = null;
var deserializer = new Atari7800Cart();
var actual = deserializer.Deserialize(data);
Assert.Null(actual);
}
[Fact]
public void EmptyStream_Null()
{
Stream? data = new MemoryStream([]);
var deserializer = new Atari7800Cart();
var actual = deserializer.Deserialize(data);
Assert.Null(actual);
}
[Fact]
public void InvalidStream_Null()
{
Stream? data = new MemoryStream([.. Enumerable.Repeat<byte>(0xFF, 1024)]);
var deserializer = new Atari7800Cart();
var actual = deserializer.Deserialize(data);
Assert.Null(actual);
}
}
}

View File

@@ -0,0 +1,61 @@
using System.IO;
using System.Linq;
using SabreTools.Serialization.Wrappers;
using Xunit;
namespace SabreTools.Serialization.Test.Wrappers
{
public class Atari7800CartTests
{
[Fact]
public void NullArray_Null()
{
byte[]? data = null;
int offset = 0;
var actual = Atari7800Cart.Create(data, offset);
Assert.Null(actual);
}
[Fact]
public void EmptyArray_Null()
{
byte[]? data = [];
int offset = 0;
var actual = Atari7800Cart.Create(data, offset);
Assert.Null(actual);
}
[Fact]
public void InvalidArray_Null()
{
byte[]? data = [.. Enumerable.Repeat<byte>(0xFF, 1024)];
int offset = 0;
var actual = Atari7800Cart.Create(data, offset);
Assert.Null(actual);
}
[Fact]
public void NullStream_Null()
{
Stream? data = null;
var actual = Atari7800Cart.Create(data);
Assert.Null(actual);
}
[Fact]
public void EmptyStream_Null()
{
Stream? data = new MemoryStream([]);
var actual = Atari7800Cart.Create(data);
Assert.Null(actual);
}
[Fact]
public void InvalidStream_Null()
{
Stream? data = new MemoryStream([.. Enumerable.Repeat<byte>(0xFF, 1024)]);
var actual = Atari7800Cart.Create(data);
Assert.Null(actual);
}
}
}

View File

@@ -0,0 +1,327 @@
using System.Collections.Generic;
using SabreTools.Data.Models.Atari7800;
namespace SabreTools.Data.Extensions
{
public static class Atari7800Cart
{
/// <summary>
/// Convert a <see cref="AudioDevice"/> value to string
/// </summary>
public static string FromAudioDevice(this AudioDevice audio)
{
string[] devices = new string[4];
byte pokey = (byte)((byte)audio & 0x0F);
devices[0] = pokey switch
{
0 => "POKEY - none",
1 => "POKEY - @440",
2 => "POKEY - @450",
3 => "POKEY - @450+@440",
4 => "POKEY - @800",
5 => "POKEY - @4000",
_ => $"Unknown {pokey}",
};
byte ym2151 = (byte)(((byte)audio >> 1) & 0x01);
devices[1] = ym2151 switch
{
0 => "No YM2151 @460",
1 => "YM2151 @460",
_ => $"Unknown {ym2151}",
};
byte covox = (byte)(((byte)audio >> 2) & 0x01);
devices[2] = covox switch
{
0 => "No COVOX @430",
1 => "COVOX @430",
_ => $"Unknown {covox}",
};
byte adpcm = (byte)(((byte)audio >> 3) & 0x01);
devices[3] = adpcm switch
{
0 => "No ADPCM Audio Stream @420",
1 => "ADPCM Audio Stream @420",
_ => $"Unknown {adpcm}",
};
return string.Join(", ", [.. devices]);
}
/// <summary>
/// Convert a <see cref="CartType"/> value to string
/// </summary>
public static string FromCartType(this CartType type)
{
List<string> types = [];
#if NET20 || NET35
if ((type & CartType.PokeyAt4000) != 0)
#else
if (type.HasFlag(CartType.PokeyAt4000))
#endif
types.Add("pokey at $4000");
#if NET20 || NET35
if ((type & CartType.SupergameBankSwitched) != 0)
#else
if (type.HasFlag(CartType.SupergameBankSwitched))
#endif
types.Add("supergame bank switched");
#if NET20 || NET35
if ((type & CartType.SupergameRamAt4000) != 0)
#else
if (type.HasFlag(CartType.SupergameRamAt4000))
#endif
types.Add("supergame ram at $4000");
#if NET20 || NET35
if ((type & CartType.RomAt4000) != 0)
#else
if (type.HasFlag(CartType.RomAt4000))
#endif
types.Add("rom at $4000");
#if NET20 || NET35
if ((type & CartType.Bank6At4000) != 0)
#else
if (type.HasFlag(CartType.Bank6At4000))
#endif
types.Add("bank 6 at $4000");
#if NET20 || NET35
if ((type & CartType.BankedRam) != 0)
#else
if (type.HasFlag(CartType.BankedRam))
#endif
types.Add("banked ram");
#if NET20 || NET35
if ((type & CartType.PokeyAt450) != 0)
#else
if (type.HasFlag(CartType.PokeyAt450))
#endif
types.Add("pokey at $450");
#if NET20 || NET35
if ((type & CartType.MirrorRamAt4000) != 0)
#else
if (type.HasFlag(CartType.MirrorRamAt4000))
#endif
types.Add("mirror ram at $4000");
#if NET20 || NET35
if ((type & CartType.ActivisionBanking) != 0)
#else
if (type.HasFlag(CartType.ActivisionBanking))
#endif
types.Add("activision banking");
#if NET20 || NET35
if ((type & CartType.AbsoluteBanking) != 0)
#else
if (type.HasFlag(CartType.AbsoluteBanking))
#endif
types.Add("absolute banking");
#if NET20 || NET35
if ((type & CartType.PokeyAt440) != 0)
#else
if (type.HasFlag(CartType.PokeyAt440))
#endif
types.Add("pokey at $440");
#if NET20 || NET35
if ((type & CartType.Ym2151At460461) != 0)
#else
if (type.HasFlag(CartType.Ym2151At460461))
#endif
types.Add("ym2151 at $460/$461");
#if NET20 || NET35
if ((type & CartType.Souper) != 0)
#else
if (type.HasFlag(CartType.Souper))
#endif
types.Add("souper");
#if NET20 || NET35
if ((type & CartType.Banksets) != 0)
#else
if (type.HasFlag(CartType.Banksets))
#endif
types.Add("banksets");
#if NET20 || NET35
if ((type & CartType.HaltBankedRam) != 0)
#else
if (type.HasFlag(CartType.HaltBankedRam))
#endif
types.Add("halt banked ram");
#if NET20 || NET35
if ((type & CartType.PokeyAt800) != 0)
#else
if (type.HasFlag(CartType.PokeyAt800))
#endif
types.Add("pokey@800");
// If no flags are set
if (types.Count == 0)
types.Add("N/A");
return string.Join(", ", [.. types]);
}
/// <summary>
/// Convert a <see cref="ControllerType"/> value to string
/// </summary>
public static string FromControllerType(this ControllerType type)
{
return type switch
{
ControllerType.None => "none",
ControllerType.Joystick => "7800 joystick",
ControllerType.Lightgun => "lightgun",
ControllerType.Paddle => "paddle",
ControllerType.Trakball => "trakball",
ControllerType.VcsJoystick => "2600 joystick",
ControllerType.VcsDriving => "2600 driving",
ControllerType.VcsKeypad => "2600 keypad",
ControllerType.STMouse => "ST mouse",
ControllerType.AmigaMouse => "Amiga mouse",
ControllerType.AtariVoxSaveKey => "AtariVox/SaveKey",
ControllerType.SNES2Atari => "SNES2Atari",
ControllerType.Mega7800 => "Mega7800",
_ => $"Unknown {(byte)type}",
};
}
/// <summary>
/// Convert a <see cref="Interrupt"/> value to string
/// </summary>
public static string FromInterrupt(this Interrupt interrupt)
{
return interrupt switch
{
Interrupt.None => "None",
Interrupt.Pokey1 => "POKEY 1 (@450 | @800 | @4000)",
Interrupt.Pokey2 => "POKEY 2 (@440)",
Interrupt.YM2151 => "YM2151",
_ => $"Unknown {(byte)interrupt}",
};
}
/// <summary>
/// Convert a <see cref="Mapper"/> value to string
/// </summary>
public static string FromMapper(this Mapper mapper)
{
return mapper switch
{
Mapper.Linear => "Linear",
Mapper.SuperGame => "SuperGame",
Mapper.Activision => "Activision",
Mapper.Absolute => "Absolute",
Mapper.Souper => "Souper",
_ => $"Unknown {(byte)mapper}",
};
}
/// <summary>
/// Convert a <see cref="MapperOptions"/> value to string
/// </summary>
public static string FromMapperOptions(this MapperOptions options)
{
string[] romOptions = new string[2];
byte option4000 = (byte)((byte)options & 0x0F);
romOptions[0] = option4000 switch
{
0 => "Option at @4000 - none",
1 => "Option at @4000 - 16K RAM",
2 => "Option at @4000 - 8K EXRAM/A8",
3 => "Option at @4000 - 32K EXRAM/M2",
4 => "Option at @4000 - EXROM",
5 => "Option at @4000 - EXFIX",
6 => "Option at @4000 - 32k EXRAM/X2",
_ => $"Unknown {option4000}",
};
byte romType = (byte)(((byte)options >> 7) & 0x01);
romOptions[1] = romType switch
{
0 => "Standard ROM",
1 => "Bankset ROM",
_ => $"Unknown {romType}",
};
return string.Join(", ", [.. romOptions]);
}
/// <summary>
/// Convert a <see cref="SaveDevice"/> value to string
/// </summary>
public static string FromSaveDevice(this SaveDevice device)
{
return device switch
{
SaveDevice.None => "None",
SaveDevice.HSC => "HSC",
SaveDevice.SaveKeyAtariVox => "SaveKey/AtariVox",
_ => $"Unknown {(byte)device}",
};
}
/// <summary>
/// Convert a <see cref="SlotPassthroughDevice"/> value to string
/// </summary>
public static string FromSlotPassthroughDevice(this SlotPassthroughDevice device)
{
return device switch
{
SlotPassthroughDevice.None => "None",
SlotPassthroughDevice.XM => "XM",
_ => $"Unknown {(byte)device}",
};
}
/// <summary>
/// Convert a <see cref="TVType"/> value to string
/// </summary>
public static string FromTVType(this TVType type)
{
string[] types = new string[3];
byte standard = (byte)((byte)type & 0x01);
types[0] = standard switch
{
0 => "NTSC",
1 => "PAL",
_ => $"Unknown {standard}",
};
byte connection = (byte)(((byte)type >> 1) & 0x01);
types[1] = connection switch
{
0 => "Component",
1 => "Composite",
_ => $"Unknown {standard}",
};
byte region = (byte)(((byte)type >> 2) & 0x01);
types[2] = region switch
{
0 => "Single-region",
1 => "Multi-region",
_ => $"Unknown {standard}",
};
return string.Join(", ", [.. types]);
}
}
}

View File

@@ -5,7 +5,7 @@ namespace SabreTools.Data.Extensions
public static class NESCart
{
/// <summary>
/// Convert a <see cref="CPUPPConsoleTypeUTiming"/> value to string
/// Convert a <see cref="ConsoleType"/> value to string
/// </summary>
public static string FromConsoleType(this ConsoleType type)
{

View File

@@ -0,0 +1,20 @@
namespace SabreTools.Data.Models.Atari7800
{
/// <summary>
/// Atari 7800 headered cart
/// </summary>
/// <see href="https://7800.8bitdev.org/index.php/A78_Header_Specification"/>
public class Cart
{
/// <summary>
/// A78 header
/// </summary>
/// <remarks>If omitted, format is technically BIN</remarks>
public Header? Header { get; set; }
/// <summary>
/// Cartridge data
/// </summary>
public byte[] Data { get; set; } = [];
}
}

View File

@@ -0,0 +1,49 @@
namespace SabreTools.Data.Models.Atari7800
{
public static class Constants
{
/// <summary>
/// A78 magic string (null-padded)
/// </summary>
public static readonly byte[] MagicBytesWithNull =
[
0x41, 0x54, 0x41, 0x52, 0x49, 0x37, 0x38, 0x30,
0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
/// <summary>
/// A78 magic string (null-padded)
/// </summary>
public const string MagicStringWithNull = "ATARI7800\0\0\0\0\0\0\0";
/// <summary>
/// A78 magic string (space-padded)
/// </summary>
public static readonly byte[] MagicBytesWithSpace =
[
0x41, 0x54, 0x41, 0x52, 0x49, 0x37, 0x38, 0x30,
0x30, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
];
/// <summary>
/// A78 magic string (space-padded)
/// </summary>
public const string MagicStringWithSpace = "ATARI7800 ";
/// <summary>
/// A78 header end magic text
/// </summary>
public static readonly byte[] HeaderEndMagicBytes =
[
0x41, 0x43, 0x54, 0x55, 0x41, 0x4C, 0x20, 0x43,
0x41, 0x52, 0x54, 0x20, 0x44, 0x41, 0x54, 0x41,
0x20, 0x53, 0x54, 0x41, 0x52, 0x54, 0x53, 0x20,
0x48, 0x45, 0x52, 0x45,
];
/// <summary>
/// A78 header end magic text
/// </summary>
public const string HeaderEndMagicString = "ACTUAL CART DATA STARTS HERE";
}
}

View File

@@ -0,0 +1,380 @@
using System;
namespace SabreTools.Data.Models.Atari7800
{
/// <summary>
/// Audio device (v4+ only)
/// </summary>
/// <see href="https://7800.8bitdev.org/index.php/A78_Header_Specification"/>
[Flags]
public enum AudioDevice : ushort
{
#region POKEY location
/// <summary>
/// POKEY - none
/// </summary>
PokeyNone = 0b00000000,
/// <summary>
/// POKEY - @440
/// </summary>
Pokey440 = 0b00000001,
/// <summary>
/// POKEY - @450
/// </summary>
Pokey450 = 0b00000010,
/// <summary>
/// POKEY - @450+@440
/// </summary>
Pokey450Plus440 = 0b00000011,
/// <summary>
/// POKEY - @800
/// </summary>
Pokey800 = 0b00000100,
/// <summary>
/// POKEY - @4000
/// </summary>
Pokey4000 = 0b00000101,
#endregion
/// <summary>
/// YM2151 @460
/// </summary>
YM2151460 = 0b00001000,
/// <summary>
/// COVOX @430
/// </summary>
COVOX430 = 0b00010000,
/// <summary>
/// ADPCM Audio Stream @420
/// </summary>
ADPCMAudioStream420 = 0b00100000,
}
/// <summary>
/// Cart type specifications
/// </summary>
/// <see href="https://7800.8bitdev.org/index.php/A78_Header_Specification"/>
[Flags]
public enum CartType : ushort
{
/// <summary>
/// pokey at $4000
/// </summary>
PokeyAt4000 = 0b00000000_00000001,
/// <summary>
/// supergame bank switched
/// </summary>
SupergameBankSwitched = 0b00000000_00000010,
/// <summary>
/// supergame ram at $4000
/// </summary>
SupergameRamAt4000 = 0b00000000_00000100,
/// <summary>
/// rom at $4000
/// </summary>
RomAt4000 = 0b00000000_00001000,
/// <summary>
/// bank 6 at $4000
/// </summary>
Bank6At4000 = 0b00000000_00010000,
/// <summary>
/// banked ram
/// </summary>
BankedRam = 0b00000000_00100000,
/// <summary>
/// pokey at $450
/// </summary>
PokeyAt450 = 0b00000000_01000000,
/// <summary>
/// mirror ram at $4000
/// </summary>
MirrorRamAt4000 = 0b00000000_10000000,
/// <summary>
/// activision banking
/// </summary>
ActivisionBanking = 0b00000001_00000000,
/// <summary>
/// absolute banking
/// </summary>
AbsoluteBanking = 0b00000010_00000000,
/// <summary>
/// pokey at $440
/// </summary>
PokeyAt440 = 0b00000100_00000000,
/// <summary>
/// ym2151 at $460/$461
/// </summary>
Ym2151At460461 = 0b00001000_00000000,
/// <summary>
/// souper
/// </summary>
Souper = 0b00010000_00000000,
/// <summary>
/// banksets
/// </summary>
Banksets = 0b00100000_00000000,
/// <summary>
/// halt banked ram
/// </summary>
HaltBankedRam = 0b01000000_00000000,
/// <summary>
/// pokey@800
/// </summary>
PokeyAt800 = 0b10000000_00000000,
}
/// <summary>
/// Controller type
/// </summary>
/// <see href="https://7800.8bitdev.org/index.php/A78_Header_Specification"/>
public enum ControllerType : byte
{
/// <summary>
/// none
/// </summary>
None = 0,
/// <summary>
/// 7800 joystick
/// </summary>
Joystick = 1,
/// <summary>
/// lightgun
/// </summary>
Lightgun = 2,
/// <summary>
/// paddle
/// </summary>
Paddle = 3,
/// <summary>
/// trakball
/// </summary>
Trakball = 4,
/// <summary>
/// 2600 joystick
/// </summary>
VcsJoystick = 5,
/// <summary>
/// 2600 driving
/// </summary>
VcsDriving = 6,
/// <summary>
/// 2600 keypad
/// </summary>
VcsKeypad = 7,
/// <summary>
/// ST mouse
/// </summary>
STMouse = 8,
/// <summary>
/// Amiga mouse
/// </summary>
AmigaMouse = 9,
/// <summary>
/// AtariVox/SaveKey
/// </summary>
AtariVoxSaveKey = 10,
/// <summary>
/// SNES2Atari
/// </summary>
SNES2Atari = 11,
/// <summary>
/// Mega7800
/// </summary>
Mega7800 = 12,
}
/// <summary>
/// Interrupt (v4+ only)
/// </summary>
/// <see href="https://7800.8bitdev.org/index.php/A78_Header_Specification"/>
[Flags]
public enum Interrupt : ushort
{
None = 0b00000000,
/// <summary>
/// POKEY 1 (@450 | @800 | @4000)
/// </summary>
Pokey1 = 0b00000001,
/// <summary>
/// POKEY 2 (@440)
/// </summary>
Pokey2 = 0b00000010,
/// <summary>
/// YM2151
/// </summary>
YM2151 = 0b00000100,
}
/// <summary>
/// Mapper (v4+ only)
/// </summary>
/// <see href="https://7800.8bitdev.org/index.php/A78_Header_Specification"/>
public enum Mapper : byte
{
Linear = 0,
SuperGame = 1,
Activision = 2,
Absolute = 3,
Souper = 4,
}
/// <summary>
/// Mapper options/details (v4+ only)
/// </summary>
/// <see href="https://7800.8bitdev.org/index.php/A78_Header_Specification"/>
[Flags]
public enum MapperOptions : byte
{
#region Linear
/// <summary>
/// Option at @4000 - none
/// </summary>
LinearNone = 0b00000000,
/// <summary>
/// Option at @4000 - 16K RAM
/// </summary>
Linear16KRAM = 0b00000001,
/// <summary>
/// Option at @4000 - 8K EXRAM/A8
/// </summary>
Linear8KEXRAMA8 = 0b00000010,
/// <summary>
/// Option at @4000 - 32K EXRAM/M2
/// </summary>
Linear32KEXRAMM2 = 0b00000011,
#endregion
#region SuperGame
/// <summary>
/// Option at @4000 - none
/// </summary>
SuperGameNone = 0b00000000,
/// <summary>
/// Option at @4000 - 16K RAM
/// </summary>
SuperGame16KRAM = 0b00000001,
/// <summary>
/// Option at @4000 - 8K EXRAM/A8
/// </summary>
SuperGame8KEXRAMA8 = 0b00000010,
/// <summary>
/// Option at @4000 - 32K EXRAM/M2
/// </summary>
SuperGame32KEXRAMM2 = 0b00000011,
/// <summary>
/// Option at @4000 - EXROM
/// </summary>
SuperGameEXROM = 0b00000100,
/// <summary>
/// Option at @4000 - EXFIX
/// </summary>
SuperGameEXFIX = 0b00000101,
/// <summary>
/// Option at @4000 - 32k EXRAM/X2
/// </summary>
SuperGame32KEXRAMX2 = 0b00000110,
#endregion
/// <summary>
/// Bankset ROM
/// </summary>
BanksetRom = 0b10000000,
}
/// <summary>
/// Save device
/// </summary>
/// <see href="https://7800.8bitdev.org/index.php/A78_Header_Specification"/>
public enum SaveDevice : byte
{
None = 0b00000000,
/// <summary>
/// HSC
/// </summary>
HSC = 0b00000001,
/// <summary>
/// SaveKey/AtariVox
/// </summary>
SaveKeyAtariVox = 0b00000010,
}
/// <summary>
/// Slot passthrough device
/// </summary>
public enum SlotPassthroughDevice : byte
{
None = 0b00000000,
XM = 0b00000001,
}
/// <summary>
/// TV type
/// </summary>
/// <see href="https://7800.8bitdev.org/index.php/A78_Header_Specification"/>
[Flags]
public enum TVType : byte
{
NTSC = 0b00000000,
PAL = 0b00000001,
Component = 0b00000000,
Composite = 0b00000010,
SingleRegion = 0b00000000,
MultiRegion = 0b00000100,
}
}

View File

@@ -0,0 +1,115 @@
namespace SabreTools.Data.Models.Atari7800
{
/// <summary>
/// Atari 7800 emulator header
/// </summary>
/// <see href="https://7800.8bitdev.org/index.php/A78_Header_Specification"/>
public class Header
{
/// <summary>
/// Header version
/// </summary>
/// <remarks>
/// Current header version is 4.
/// If no V4 features are used, this should be set to 3.
/// </remarks>
public byte HeaderVersion { get; set; }
/// <summary>
/// "ATARI7800" with 7 null bytes following
/// </summary>
public byte[] MagicText { get; set; } = new byte[16];
/// <summary>
/// Cart title, encoding not specified
/// </summary>
public byte[] CartTitle { get; set; } = new byte[32];
/// <summary>
/// ROM size without header data
/// </summary>
/// <remarks>
/// Endianness possibly varies by version?
/// Version 1 - Big-endian
/// Version 2 - ?????
/// Version 3 - ?????
/// Version 4 - ?????
/// </remarks>
public uint RomSizeWithoutHeader { get; set; }
/// <summary>
/// Cart type flags
/// </summary>
public CartType CartType { get; set; }
/// <summary>
/// Controller 1 type
/// </summary>
public ControllerType Controller1Type { get; set; }
/// <summary>
/// Controller 2 type
/// </summary>
public ControllerType Controller2Type { get; set; }
/// <summary>
/// TV type
/// </summary>
public TVType TVType { get; set; }
/// <summary>
/// Save device
/// </summary>
public SaveDevice SaveDevice { get; set; }
/// <summary>
/// Reserved
/// </summary>
public byte[] Reserved { get; set; } = new byte[4];
/// <summary>
/// Slot passthrough device
/// </summary>
public SlotPassthroughDevice SlotPassthroughDevice { get; set; }
#region Version 4+
/// <summary>
/// Mapper
/// </summary>
/// <remarks>v4+ only</remarks>
public Mapper Mapper { get; set; }
/// <summary>
/// Mapper options
/// </summary>
/// <remarks>v4+ only</remarks>
public MapperOptions MapperOptions { get; set; }
/// <summary>
/// Audio device(s)
/// </summary>
public AudioDevice AudioDevice { get; set; }
/// <summary>
/// Interrupt
/// </summary>
public Interrupt Interrupt { get; set; }
#endregion
/// <summary>
/// Padding from end of header data to end magic text
/// </summary>
/// <remarks>
/// 36 bytes in versions lower than 4.
/// 30 bytes in versions 4 and above.
/// </remarks>
public byte[] Padding { get; set; } = [];
/// <summary>
/// "ACTUAL CART DATA STARTS HERE"
/// </summary>
public byte[] EndMagicText { get; set; } = new byte[28];
}
}

View File

@@ -11,6 +11,7 @@ Not all of this information was able to be gathered directly from the files in q
| [3DBrew](https://www.3dbrew.org/wiki/Main_Page) | N3DS |
| [Aaru Data Preservation Suite](https://github.com/aaru-dps) | PIC |
| [Advanced Access Content System Licensing Administrator (AACS LA)](https://aacsla.com/) | AACS |
| [Atari 7800 Development Wiki](https://7800.8bitdev.org/index.php/Main_Page) | Atari7800Cart |
| [BYTE*](https://web.archive.org/web/20240703222951/https://bytepointer.com/index.htm) | NewExecutable |
| [cabextract/libmspack](https://www.cabextract.org.uk/) | LZ |
| [DBox](https://dbox.tools/) | Xbox |

View File

@@ -0,0 +1,96 @@
using System.IO;
using SabreTools.Data.Models.Atari7800;
using SabreTools.IO.Extensions;
using static SabreTools.Data.Models.Atari7800.Constants;
namespace SabreTools.Serialization.Readers
{
public class Atari7800Cart : BaseBinaryReader<Cart>
{
/// <inheritdoc/>
public override Cart? Deserialize(Stream? data)
{
// If the data is invalid
if (data is null || !data.CanRead)
return null;
try
{
// Cache the current offset
long initialOffset = data.Position;
// Create a new A78 cart image to fill
var cart = new Cart();
#region Header
// Try to parse the header
var header = ParseHeader(data);
if (!header.MagicText.EqualsExactly(MagicBytesWithNull)
&& !header.MagicText.EqualsExactly(MagicBytesWithSpace))
{
return null;
}
else if (!header.EndMagicText.EqualsExactly(HeaderEndMagicBytes))
{
return null;
}
// Set the header
cart.Header = header;
#endregion
// Read the cart data, if necessary
if (cart.Header.RomSizeWithoutHeader > 0)
cart.Data = data.ReadBytes((int)cart.Header.RomSizeWithoutHeader);
return cart;
}
catch
{
// Ignore the actual error
return null;
}
}
/// <summary>
/// Parse a Stream into a Header
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled Header on success, null on error</returns>
public static Header ParseHeader(Stream data)
{
var obj = new Header();
obj.HeaderVersion = data.ReadByteValue();
obj.MagicText = data.ReadBytes(16);
obj.CartTitle = data.ReadBytes(32);
obj.RomSizeWithoutHeader = data.ReadUInt32BigEndian();
obj.CartType = (CartType)data.ReadUInt16LittleEndian();
obj.Controller1Type = (ControllerType)data.ReadByteValue();
obj.Controller2Type = (ControllerType)data.ReadByteValue();
obj.TVType = (TVType)data.ReadByteValue();
obj.SaveDevice = (SaveDevice)data.ReadByteValue();
obj.Reserved = data.ReadBytes(4);
obj.SlotPassthroughDevice = (SlotPassthroughDevice)data.ReadByteValue();
if (obj.HeaderVersion >= 4)
{
obj.Mapper = (Mapper)data.ReadByteValue();
obj.MapperOptions = (MapperOptions)data.ReadByteValue();
obj.AudioDevice = (AudioDevice)data.ReadUInt16LittleEndian();
obj.Interrupt = (Interrupt)data.ReadUInt16LittleEndian();
obj.Padding = data.ReadBytes(30);
}
else
{
obj.Padding = data.ReadBytes(36);
}
obj.EndMagicText = data.ReadBytes(28);
return obj;
}
}
}

View File

@@ -26,7 +26,7 @@ namespace SabreTools.Serialization.Readers
// Try to parse the header
var header = ParseHeader(data);
if (header == null)
if (header is null)
return null;
// Set the header

View File

@@ -20,7 +20,7 @@ namespace SabreTools.Serialization.Readers
public override Data.Models.VDF.SkuSis? Deserialize(Stream? data)
{
// If the data is invalid
if (data == null || !data.CanRead)
if (data is null || !data.CanRead)
return null;
try
@@ -39,12 +39,12 @@ namespace SabreTools.Serialization.Readers
data.SeekIfPossible(initialOffset, SeekOrigin.Begin);
var jsonBytes = ParseSkuSis(data);
if (jsonBytes == null)
if (jsonBytes is null)
return null;
var deserializer = new SkuSisJson();
var skuSisJson = deserializer.Deserialize(jsonBytes, 0);
if (skuSisJson == null)
if (skuSisJson is null)
return null;
return skuSisJson;
@@ -106,7 +106,7 @@ namespace SabreTools.Serialization.Readers
while (!reader.EndOfStream)
{
string? line = reader.ReadLine();
if (line == null)
if (line is null)
continue;
// Curly braces are always on their own lines

View File

@@ -15,6 +15,7 @@ namespace SabreTools.Serialization
return fileType switch
{
WrapperType.AACSMediaKeyBlock => AACSMediaKeyBlock.Create(data),
WrapperType.Atari7800Cart => Atari7800Cart.Create(data),
WrapperType.BDPlusSVM => BDPlusSVM.Create(data),
WrapperType.BFPK => BFPK.Create(data),
WrapperType.BSP => BSP.Create(data),
@@ -168,6 +169,30 @@ namespace SabreTools.Serialization
#endregion
// TODO: Figure out how to use constants here
#region Atari7800Cart
// Version 1 header
if (magic.StartsWith([0x01, 0x41, 0x54, 0x41, 0x52, 0x49, 0x37, 0x38, 0x30, 0x30]))
return WrapperType.Atari7800Cart;
// Version 2 header
if (magic.StartsWith([0x02, 0x41, 0x54, 0x41, 0x52, 0x49, 0x37, 0x38, 0x30, 0x30]))
return WrapperType.Atari7800Cart;
// Version 3 header
if (magic.StartsWith([0x03, 0x41, 0x54, 0x41, 0x52, 0x49, 0x37, 0x38, 0x30, 0x30]))
return WrapperType.Atari7800Cart;
// Version 4 header
if (magic.StartsWith([0x04, 0x41, 0x54, 0x41, 0x52, 0x49, 0x37, 0x38, 0x30, 0x30]))
return WrapperType.Atari7800Cart;
if (extension.Equals("a78", StringComparison.OrdinalIgnoreCase))
return WrapperType.Atari7800Cart;
#endregion
#region BDPlusSVM
if (magic.StartsWith(Data.Models.BDPlus.Constants.SignatureBytes))

View File

@@ -0,0 +1,143 @@
using System;
using System.IO;
using SabreTools.Data.Models.Atari7800;
using SabreTools.IO.Extensions;
namespace SabreTools.Serialization.Wrappers
{
public partial class Atari7800Cart : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// Get the base path
string baseFilename = Filename is null
? Guid.NewGuid().ToString()
: Path.GetFileNameWithoutExtension(Filename);
string basePath = Path.Combine(outputDirectory, baseFilename);
// Check if any data was extracted successfully
bool success = false;
// Header data
if (Header is not null)
{
string headerPath = $"{basePath}.hdr";
if (includeDebug) Console.WriteLine($"Attempting to extract header data to {headerPath}");
// Try to write the data
try
{
// Open the output file for writing
using var fs = File.Open(headerPath, FileMode.Create, FileAccess.Write, FileShare.None);
// Byte 0
fs.Write(Header.HeaderVersion);
fs.Flush();
// Bytes 1-16
fs.Write(Header.MagicText);
fs.Flush();
// Bytes 17-48
fs.Write(Header.CartTitle);
fs.Flush();
// Bytes 49-52
fs.Write(Header.RomSizeWithoutHeader);
fs.Flush();
// Bytes 53-54
fs.Write((ushort)Header.CartType);
fs.Flush();
// Bytes 55-56
fs.Write((byte)Header.Controller1Type);
fs.Write((byte)Header.Controller2Type);
fs.Flush();
// Byte 57
fs.Write((byte)Header.TVType);
fs.Flush();
// Byte 58
fs.Write((byte)Header.SaveDevice);
fs.Flush();
// Bytes 59-62
fs.Write(Header.Reserved);
fs.Flush();
// Byte 63
fs.Write((byte)Header.SlotPassthroughDevice);
fs.Flush();
if (HeaderVersion >= 4)
{
// Byte 64
fs.Write((byte)Header.Mapper);
fs.Flush();
// Byte 65
fs.Write((byte)Header.MapperOptions);
fs.Flush();
// Byte 66-67
fs.Write((ushort)Header.AudioDevice);
fs.Flush();
// Byte 68-69
fs.Write((ushort)Header.Interrupt);
fs.Flush();
// Bytes 70-99
fs.Write(Header.Padding);
fs.Flush();
}
else
{
// Bytes 64-99
fs.Write(Header.Padding);
fs.Flush();
}
// Bytes 100-127
fs.Write(Header.EndMagicText);
fs.Flush();
// Header extracted
success = true;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
}
}
// ROM data
if (Model.Data.Length > 0)
{
string romPath = $"{basePath}.bin";
if (includeDebug) Console.WriteLine($"Attempting to extract ROM data to {romPath}");
// Try to write the data
try
{
// Open the output file for writing
using var fs = File.Open(romPath, FileMode.Create, FileAccess.Write, FileShare.None);
fs.Write(Model.Data, 0, Model.Data.Length);
fs.Flush();
// ROM extracted
success = true;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
}
}
return success;
}
}
}

View File

@@ -0,0 +1,91 @@
using System.Text;
using SabreTools.Data.Extensions;
using SabreTools.Data.Models.Atari7800;
namespace SabreTools.Serialization.Wrappers
{
public partial class Atari7800Cart : IPrintable
{
#if NETCOREAPP
/// <inheritdoc/>
public string ExportJSON() => System.Text.Json.JsonSerializer.Serialize(Model, _jsonSerializerOptions);
#endif
/// <inheritdoc/>
public void PrintInformation(StringBuilder builder)
{
builder.AppendLine("Atari 7800 Cart Information:");
builder.AppendLine("-------------------------");
builder.AppendLine();
Print(builder, Model.Header);
//builder.AppendLine(Model.Trainer, "Trainer Data");
builder.AppendLine(Model.Data.Length, "ROM Data Length");
}
private static void Print(StringBuilder builder, Header? header)
{
builder.AppendLine(" Header Information:");
builder.AppendLine(" -------------------------");
if (header is null)
{
builder.AppendLine(" No header present");
builder.AppendLine();
return;
}
builder.AppendLine(header.HeaderVersion, " Header version");
builder.AppendLine(header.MagicText, " Magic text");
builder.AppendLine(Encoding.ASCII.GetString(header.MagicText).TrimEnd('\0'), " Magic text (ASCII)");
builder.AppendLine(header.CartTitle, " Cart title");
builder.AppendLine(Encoding.ASCII.GetString(header.CartTitle).TrimEnd('\0'), " Cart title (ASCII)");
builder.AppendLine(header.RomSizeWithoutHeader, " ROM size without header");
string cartType = header.CartType.FromCartType();
builder.AppendLine(cartType, " Cart type flags");
string controller1Type = header.Controller1Type.FromControllerType();
builder.AppendLine(controller1Type, " Controller 1 type");
string controller2Type = header.Controller2Type.FromControllerType();
builder.AppendLine(controller2Type, " Controller 2 type");
string tvType = header.TVType.FromTVType();
builder.AppendLine(tvType, " TV type");
string saveDevice = header.SaveDevice.FromSaveDevice();
builder.AppendLine(saveDevice, " Save device");
builder.AppendLine(header.Reserved, " Reserved");
string slotPassthroughDevice = header.SlotPassthroughDevice.FromSlotPassthroughDevice();
builder.AppendLine(slotPassthroughDevice, " Slot passthrough device");
if (header.HeaderVersion >= 4)
{
string mapper = header.Mapper.FromMapper();
builder.AppendLine(mapper, " Mapper");
string mapperOptions = header.MapperOptions.FromMapperOptions();
builder.AppendLine(mapperOptions, " Mapper options");
string audioDevice = header.AudioDevice.FromAudioDevice();
builder.AppendLine(audioDevice, " Audio device");
string interrupt = header.Interrupt.FromInterrupt();
builder.AppendLine(interrupt, " Interrupt");
builder.AppendLine(header.Padding, " Padding");
}
else
{
builder.AppendLine(header.Padding, " Padding");
}
builder.AppendLine(header.EndMagicText, " End magic text");
builder.AppendLine(Encoding.ASCII.GetString(header.EndMagicText).TrimEnd('\0'), " End magic text (ASCII)");
builder.AppendLine();
}
}
}

View File

@@ -0,0 +1,147 @@
using System.IO;
using System.Text;
using SabreTools.Data.Models.Atari7800;
namespace SabreTools.Serialization.Wrappers
{
public partial class Atari7800Cart : WrapperBase<Cart>
{
#region Descriptive Properties
/// <inheritdoc/>
public override string DescriptionString => "Atari 7800 Cart Image";
#endregion
#region Extension Properties
/// <inheritdoc cref="Cart.Header"/>
public Header? Header => Model.Header;
/// <inheritdoc cref="Header.AudioDevice"/>
public AudioDevice AudioDevice => Header?.AudioDevice ?? 0;
/// <inheritdoc cref="Header.CartTitle"/>
public string? CartTitle
{
get
{
// Missing header
if (Header is null)
return null;
return Encoding.ASCII.GetString(Header.CartTitle);
}
}
/// <inheritdoc cref="Header.CartType"/>
public CartType CartType => Header?.CartType ?? 0;
/// <inheritdoc cref="Header.Controller1Type"/>
public ControllerType Controller1Type => Header?.Controller1Type ?? 0;
/// <inheritdoc cref="Header.Controller2Type"/>
public ControllerType Controller2Type => Header?.Controller2Type ?? 0;
/// <inheritdoc cref="Header.HeaderVersion"/>
public byte HeaderVersion => Header?.HeaderVersion ?? 0;
/// <inheritdoc cref="Header.Interrupt"/>
public Interrupt Interrupt => Header?.Interrupt ?? 0;
/// <inheritdoc cref="Header.Mapper"/>
public Mapper Mapper => Header?.Mapper ?? 0;
/// <inheritdoc cref="Header.MapperOptions"/>
public MapperOptions MapperOptions => Header?.MapperOptions ?? 0;
/// <inheritdoc cref="Header.RomSizeWithoutHeader"/>
public uint RomSizeWithoutHeader => Header?.RomSizeWithoutHeader ?? 0;
/// <inheritdoc cref="Header.SaveDevice"/>
public SaveDevice SaveDevice => Header?.SaveDevice ?? 0;
/// <inheritdoc cref="Header.SlotPassthroughDevice"/>
public SlotPassthroughDevice SlotPassthroughDevice => Header?.SlotPassthroughDevice ?? 0;
/// <inheritdoc cref="Header.TVType"/>
public TVType TVType => Header?.TVType ?? 0;
#endregion
#region Constructors
/// <inheritdoc/>
public Atari7800Cart(Cart model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public Atari7800Cart(Cart model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public Atari7800Cart(Cart model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public Atari7800Cart(Cart model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public Atari7800Cart(Cart model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public Atari7800Cart(Cart model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create an Atari 7800 cart image from a byte array and offset
/// </summary>
/// <param name="data">Byte array representing the archive</param>
/// <param name="offset">Offset within the array to parse</param>
/// <returns>An Atari 7800 cart image wrapper on success, null on failure</returns>
public static Atari7800Cart? Create(byte[]? data, int offset)
{
// If the data is invalid
if (data is null || data.Length == 0)
return null;
// If the offset is out of bounds
if (offset < 0 || offset >= data.Length)
return null;
// Create a memory stream and use that
var dataStream = new MemoryStream(data, offset, data.Length - offset);
return Create(dataStream);
}
/// <summary>
/// Create an Atari 7800 cart image from a Stream
/// </summary>
/// <param name="data">Stream representing the archive</param>
/// <returns>An Atari 7800 cart image wrapper on success, null on failure</returns>
public static Atari7800Cart? Create(Stream? data)
{
// If the data is invalid
if (data is null || !data.CanRead)
return null;
try
{
// Cache the current offset
long currentOffset = data.Position;
var model = new Readers.Atari7800Cart().Deserialize(data);
if (model is null)
return null;
return new Atari7800Cart(model, data, currentOffset);
}
catch
{
return null;
}
}
#endregion
}
}

View File

@@ -95,7 +95,7 @@ namespace SabreTools.Serialization.Wrappers
public static SkuSis? Create(byte[]? data, int offset)
{
// If the data is invalid
if (data == null || data.Length == 0)
if (data is null || data.Length == 0)
return null;
// If the offset is out of bounds
@@ -115,7 +115,7 @@ namespace SabreTools.Serialization.Wrappers
public static SkuSis? Create(Stream? data)
{
// If the data is invalid
if (data == null || !data.CanRead)
if (data is null || !data.CanRead)
return null;
try
@@ -124,7 +124,7 @@ namespace SabreTools.Serialization.Wrappers
long currentOffset = data.Position;
var model = new Readers.SkuSis().Deserialize(data);
if (model == null)
if (model is null)
return null;
return new SkuSis(model, data, currentOffset);

View File

@@ -15,6 +15,11 @@ namespace SabreTools.Serialization.Wrappers
/// </summary>
AACSMediaKeyBlock,
/// <summary>
/// Atari7800 cart image
/// </summary>
Atari7800Cart,
/// <summary>
/// BD+ SVM
/// </summary>