First round of cleanup for DolphinLib additions

This commit is contained in:
Matt Nadareski
2026-05-12 20:33:53 -04:00
parent 49aa6895b6
commit 2a0e8e2eb0
53 changed files with 3861 additions and 2586 deletions

View File

@@ -1,5 +1,7 @@
using System.Collections.Generic;
using System.IO;
using System.Text;
using SabreTools.Data.Extensions;
using SabreTools.Data.Models.NintendoDisc;
using SabreTools.IO.Extensions;
using SabreTools.Numerics.Extensions;
@@ -31,31 +33,27 @@ namespace SabreTools.Serialization.Readers
// Determine platform from magic words; fall back to GameId prefix for
// GC discs that omit the magic word (e.g. some redump/scene ISOs)
if (disc.Header.WiiMagic == Constants.WiiMagicWord)
disc.Platform = Platform.Wii;
else if (disc.Header.GCMagic == Constants.GCMagicWord)
disc.Platform = Platform.GameCube;
else if (disc.Header.GameId != null && disc.Header.GameId.Length >= 1
&& IsGameCubeTitleType(disc.Header.GameId[0]))
disc.Platform = Platform.GameCube;
else
disc.Platform = Platform.Unknown;
Platform platform = disc.Header.GetPlatform();
// Parse Wii-specific structures
if (disc.Platform == Platform.Wii)
if (platform == Platform.Wii)
{
// Partition table starts at 0x40000
long partTableEnd = initialOffset + Constants.WiiPartitionTableAddress
long partTableEnd = initialOffset
+ Constants.WiiPartitionTableAddress
+ (Constants.WiiPartitionGroupCount * 8);
if (data.Length >= partTableEnd)
if (partTableEnd < data.Length)
{
data.Seek(initialOffset + Constants.WiiPartitionTableAddress, SeekOrigin.Begin);
disc.PartitionTableEntries = ParsePartitionTable(data, initialOffset);
}
// Region data at 0x4E000
long regionEnd = initialOffset + Constants.WiiRegionDataAddress + Constants.WiiRegionDataSize;
if (data.Length >= regionEnd)
if (regionEnd < data.Length)
{
data.Seek(initialOffset + Constants.WiiRegionDataAddress, SeekOrigin.Begin);
disc.RegionData = ParseRegionData(data);
disc.RegionData = ParseWiiRegionData(data);
}
}
@@ -67,139 +65,215 @@ namespace SabreTools.Serialization.Readers
}
}
#region Header parsing
/// <summary>
/// Parses just the disc header fields from the given stream without requiring
/// the full 0x440-byte boot block. Requires at least 0x82 bytes (enough to
/// reach AudioStreaming and StreamingBufferSize) to be useful; the DOL/FST
/// fields will be zero when the stream is shorter than 0x42B bytes.
/// Parse a Stream into a DiscHeader
/// </summary>
public static DiscHeader? ParseDiscHeaderOnly(Stream? data)
/// <param name="data">Stream to parse</param>
/// <returns>Filled DiscHeader on success, null on error</returns>
public static DiscHeader ParseDiscHeader(Stream data)
{
if (data is null || !data.CanRead || data.Length - data.Position < 6)
return null;
try { return ParseDiscHeader(data); }
catch { return null; }
}
private static DiscHeader ParseDiscHeader(Stream data)
{
var header = new DiscHeader();
var obj = new DiscHeader();
// 0x000: 4-char title code + 2-char maker code stored as one 6-byte GameId field
byte[] gameIdBytes = data.ReadBytes(Constants.GameIdLength);
header.GameId = Encoding.ASCII.GetString(gameIdBytes).TrimEnd('\0');
byte[] gameIdBytes = data.ReadBytes(6);
obj.GameId = Encoding.ASCII.GetString(gameIdBytes);
// Maker code is the last 2 chars of the GameId (offsets 0x0040x005).
// Dolphin reads it with Read(0x4, 2) — there is no separate field at 0x006.
header.MakerCode = header.GameId != null && header.GameId.Length >= 6
? header.GameId.Substring(4, 2)
: string.Empty;
obj.DiscNumber = data.ReadByteValue();
obj.DiscVersion = data.ReadByteValue();
obj.AudioStreaming = data.ReadByteValue();
obj.StreamingBufferSize = data.ReadByteValue();
obj.Padding00A = data.ReadBytes(0x0E);
obj.WiiMagic = data.ReadUInt32BigEndian();
obj.GCMagic = data.ReadUInt32BigEndian();
// 0x006: disc number, 0x007: revision (Dolphin GetDiscNumber/GetRevision)
header.DiscNumber = data.ReadByteValue();
header.DiscVersion = data.ReadByteValue();
// 0x008: audio streaming, 0x009: streaming buffer size
header.AudioStreaming = data.ReadByteValue();
header.StreamingBufferSize = data.ReadByteValue();
byte[] titleBytes = data.ReadBytes(0x60);
obj.GameTitle = Encoding.ASCII.GetString(titleBytes);
// Skip unused 0x0E bytes (offsets 0x00A0x017)
data.ReadBytes(0x0E);
obj.DisableHashVerification = data.ReadByteValue();
obj.DisableDiscEncryption = data.ReadByteValue();
obj.Padding082 = data.ReadBytes(0x39E);
obj.DolOffset = data.ReadUInt32BigEndian();
obj.FstOffset = data.ReadUInt32BigEndian();
obj.FstSize = data.ReadUInt32BigEndian();
obj.Padding42C = data.ReadBytes(0x14);
header.WiiMagic = data.ReadUInt32BigEndian();
header.GCMagic = data.ReadUInt32BigEndian();
byte[] titleBytes = data.ReadBytes(Constants.GameTitleLength);
header.GameTitle = Encoding.ASCII.GetString(titleBytes).TrimEnd('\0');
header.DisableHashVerification = data.Position < data.Length ? data.ReadByteValue() : (byte)0;
header.DisableDiscEncryption = data.Position < data.Length ? data.ReadByteValue() : (byte)0;
// Skip to DOL/FST offset fields at 0x420.
// Position so far: 6+1+1+1+1+14+4+4+96+1+1 = 130 = 0x82
int skipToBootBlock = Constants.DolOffsetField - 0x82;
if (data.Length - data.Position < skipToBootBlock + 12)
return header;
data.ReadBytes(skipToBootBlock);
header.DolOffset = data.ReadUInt32BigEndian();
header.FstOffset = data.ReadUInt32BigEndian();
header.FstSize = data.ReadUInt32BigEndian();
// Skip the remaining bytes to complete the 0x440 header
// We are at 0x420 + 12 = 0x42C; need to reach 0x440
data.ReadBytes(Constants.DiscHeaderSize - (Constants.DolOffsetField + 12));
return header;
return obj;
}
#endregion
#region Wii partition table parsing
private static WiiPartitionTableEntry[]? ParsePartitionTable(Stream data, long baseOffset)
{
data.Seek(baseOffset + Constants.WiiPartitionTableAddress, SeekOrigin.Begin);
// Read 4 partition groups; each group has a count and a shifted offset
var allEntries = new System.Collections.Generic.List<WiiPartitionTableEntry>();
for (int g = 0; g < Constants.WiiPartitionGroupCount; g++)
{
uint count = data.ReadUInt32BigEndian();
uint shiftedOffset = data.ReadUInt32BigEndian();
if (count == 0)
continue;
long tableOffset = baseOffset + ((long)shiftedOffset << 2);
long savedPosition = data.Position;
if (tableOffset + ((long)count * 8) > data.Length)
{
data.Seek(savedPosition, SeekOrigin.Begin);
continue;
}
data.Seek(tableOffset, SeekOrigin.Begin);
for (uint i = 0; i < count; i++)
{
var entry = new WiiPartitionTableEntry();
uint rawOffset = data.ReadUInt32BigEndian();
entry.Offset = (long)rawOffset << 2;
entry.Type = data.ReadUInt32BigEndian();
allEntries.Add(entry);
}
data.Seek(savedPosition, SeekOrigin.Begin);
}
return allEntries.Count > 0 ? allEntries.ToArray() : null;
}
#endregion
#region Wii region data parsing
private static WiiRegionData ParseRegionData(Stream data)
{
var region = new WiiRegionData();
region.RegionSetting = data.ReadUInt32BigEndian();
region.AgeRatings = data.ReadBytes(16);
return region;
}
#endregion
/// <summary>
/// Returns true if the GameId first character is a known GameCube title type prefix.
/// Used as a fallback when the GC magic word is absent from the disc image.
/// Parse a byte array into a DOLHeader
/// </summary>
private static bool IsGameCubeTitleType(char c)
/// <param name="data">Byte array to parse</param>
/// <returns>Filled DOLHeader on success, null on error</returns>
public static DOLHeader ParseDOLHeader(byte[] data, ref int offset)
{
return c == 'G' || c == 'D' || c == 'R';
var obj = new DOLHeader();
obj.SectionOffsetTable = new uint[18];
for (int i = 0; i < obj.SectionOffsetTable.Length; i++)
{
obj.SectionOffsetTable[i] = data.ReadUInt32BigEndian(ref offset);
}
obj.SectionAddressTable = new uint[18];
for (int i = 0; i < obj.SectionAddressTable.Length; i++)
{
obj.SectionAddressTable[i] = data.ReadUInt32BigEndian(ref offset);
}
obj.SectionLengthsTable = new uint[18];
for (int i = 0; i < obj.SectionLengthsTable.Length; i++)
{
obj.SectionLengthsTable[i] = data.ReadUInt32BigEndian(ref offset);
}
obj.BSSAddress = data.ReadUInt32BigEndian(ref offset);
obj.BSSLength = data.ReadUInt32BigEndian(ref offset);
obj.EntryPoint = data.ReadUInt32BigEndian(ref offset);
obj.Padding = data.ReadBytes(ref offset, 0x1C);
return obj;
}
/// <summary>
/// Parse a byte array into a ParseFileSystemTable
/// </summary>
/// <param name="data">Byte array to parse</param>
/// <returns>Filled ParseFileSystemTable on success, null on error</returns>
public static FileSystemTable? ParseFileSystemTable(byte[] data)
{
// Check that the root entry exists
if (data.Length < 12)
return null;
var obj = new FileSystemTable();
// Read the root entry first
int offset = 0;
obj.Unknown = data.ReadBytes(ref offset, 8);
obj.EntryCount = data.ReadUInt32BigEndian(ref offset);
if (obj.EntryCount < 1 || (obj.EntryCount * 12) > data.Length)
return null;
// Read all entries
offset = 0;
obj.Entries = new FileSystemTableEntry[obj.EntryCount];
for (int i = 0; i < obj.Entries.Length; i++)
{
obj.Entries[i] = ParseFileSystemTableEntry(data, ref offset);
}
return obj;
}
/// <summary>
/// Parse a byte array into a FileSystemTableEntry
/// </summary>
/// <param name="data">Byte array to parse</param>
/// <returns>Filled FileSystemTableEntry on success, null on error</returns>
public static FileSystemTableEntry ParseFileSystemTableEntry(byte[] data, ref int offset)
{
var obj = new FileSystemTableEntry();
obj.NameOffset = data.ReadUInt32BigEndian(ref offset);
obj.FileOffset = data.ReadUInt32BigEndian(ref offset);
obj.FileSize = data.ReadUInt32BigEndian(ref offset);
return obj;
}
/// <summary>
/// Parse a Stream into a partition table
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled partition table on success, null on error</returns>
public static WiiPartitionTableEntry[]? ParsePartitionTable(Stream data, long initialOffset)
{
// Read 4 partition groups; each group has a count and a shifted offset
var allEntries = new List<WiiPartitionTableEntry>();
for (int i = 0; i < Constants.WiiPartitionGroupCount; i++)
{
var group = ParseWiiPartitionGroup(data, initialOffset);
if (group.Count == 0)
continue;
// TODO: Keep group entries separate
allEntries.AddRange(group.Entries);
}
return allEntries.Count > 0 ? [.. allEntries] : null;
}
/// <summary>
/// Parse a Stream into a WiiPartitionGroup
/// </summary>
/// <param name="data">Stream to parse</param>
/// <param name="initialOffset">Initial offset into the stream</param>
/// <returns>Filled WiiPartitionTableEntry on success, null on error</returns>
public static WiiPartitionGroup ParseWiiPartitionGroup(Stream data, long initialOffset)
{
var obj = new WiiPartitionGroup();
obj.Count = data.ReadUInt32BigEndian();
obj.Offset = data.ReadUInt32BigEndian();
// Empty groups should not attempt parsing
if (obj.Count == 0)
return obj;
// Determine the table offset
long tableOffset = initialOffset + (obj.Offset << 2);
if (tableOffset + (obj.Count * 8) > data.Length)
return obj;
// Seek to the table, if possible
long savedPosition = data.Position;
data.Seek(tableOffset, SeekOrigin.Begin);
// Parse the table
List<WiiPartitionTableEntry> entries = [];
for (uint i = 0; i < obj.Count; i++)
{
var entry = ParseWiiPartitionTableEntry(data);
entries.Add(entry);
}
// Set the entries and reset the stream
obj.Entries = [.. entries];
data.Seek(savedPosition, SeekOrigin.Begin);
return obj;
}
/// <summary>
/// Parse a Stream into a WiiPartitionTableEntry
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled WiiPartitionTableEntry on success, null on error</returns>
public static WiiPartitionTableEntry ParseWiiPartitionTableEntry(Stream data)
{
var obj = new WiiPartitionTableEntry();
obj.Offset = data.ReadUInt32BigEndian();
obj.Type = data.ReadUInt32BigEndian();
return obj;
}
/// <summary>
/// Parse a Stream into a WiiRegionData
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled WiiRegionData on success, null on error</returns>
public static WiiRegionData ParseWiiRegionData(Stream data)
{
var obj = new WiiRegionData();
obj.RegionSetting = data.ReadUInt32BigEndian();
obj.AgeRatings = data.ReadBytes(16);
return obj;
}
}
}