mirror of
https://github.com/SabreTools/SabreTools.Serialization.git
synced 2026-07-08 18:06:41 +00:00
Add Atari 7800 cart support
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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('.');
|
||||
|
||||
@@ -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 |
|
||||
|
||||
73
SabreTools.Serialization.Test/Readers/Atari7800CartTests.cs
Normal file
73
SabreTools.Serialization.Test/Readers/Atari7800CartTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
61
SabreTools.Serialization.Test/Wrappers/Atari7800CartTests.cs
Normal file
61
SabreTools.Serialization.Test/Wrappers/Atari7800CartTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
327
SabreTools.Serialization/Extensions/Atari7800Cart.cs
Normal file
327
SabreTools.Serialization/Extensions/Atari7800Cart.cs
Normal 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
20
SabreTools.Serialization/Models/Atari7800/Cart.cs
Normal file
20
SabreTools.Serialization/Models/Atari7800/Cart.cs
Normal 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; } = [];
|
||||
}
|
||||
}
|
||||
49
SabreTools.Serialization/Models/Atari7800/Constants.cs
Normal file
49
SabreTools.Serialization/Models/Atari7800/Constants.cs
Normal 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";
|
||||
}
|
||||
}
|
||||
380
SabreTools.Serialization/Models/Atari7800/Enums.cs
Normal file
380
SabreTools.Serialization/Models/Atari7800/Enums.cs
Normal 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,
|
||||
}
|
||||
}
|
||||
115
SabreTools.Serialization/Models/Atari7800/Header.cs
Normal file
115
SabreTools.Serialization/Models/Atari7800/Header.cs
Normal 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];
|
||||
}
|
||||
}
|
||||
@@ -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 |
|
||||
|
||||
96
SabreTools.Serialization/Readers/Atari7800Cart.cs
Normal file
96
SabreTools.Serialization/Readers/Atari7800Cart.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
143
SabreTools.Serialization/Wrappers/Atari7800Cart.Extraction.cs
Normal file
143
SabreTools.Serialization/Wrappers/Atari7800Cart.Extraction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
91
SabreTools.Serialization/Wrappers/Atari7800Cart.Printing.cs
Normal file
91
SabreTools.Serialization/Wrappers/Atari7800Cart.Printing.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
147
SabreTools.Serialization/Wrappers/Atari7800Cart.cs
Normal file
147
SabreTools.Serialization/Wrappers/Atari7800Cart.cs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -15,6 +15,11 @@ namespace SabreTools.Serialization.Wrappers
|
||||
/// </summary>
|
||||
AACSMediaKeyBlock,
|
||||
|
||||
/// <summary>
|
||||
/// Atari7800 cart image
|
||||
/// </summary>
|
||||
Atari7800Cart,
|
||||
|
||||
/// <summary>
|
||||
/// BD+ SVM
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user