Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions ExtractionTool/Features/MainFeature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,8 @@ private void ExtractFile(string file)
Console.WriteLine($"Attempting to extract all files from {file}");
using Stream stream = File.Open(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);

// Read the first 16 bytes
byte[] magic = stream.PeekBytes(16);
// Read the first 32 bytes — needed to detect NintendoDisc magic at 0x18/0x1C
Comment thread
Dimensional marked this conversation as resolved.
byte[] magic = stream.PeekBytes(32);

// Get the file type
string extension = Path.GetExtension(file).TrimStart('.');
Expand Down
4 changes: 2 additions & 2 deletions InfoPrint/Features/MainFeature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,8 @@ private void PrintFileInfo(string file)
{
using Stream stream = File.Open(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);

// Read the first 16 bytes
byte[] magic = stream.PeekBytes(16);
// Read the first 32 bytes — needed to detect NintendoDisc magic at 0x18/0x1C
Comment thread
Dimensional marked this conversation as resolved.
byte[] magic = stream.PeekBytes(32);

// Get the file type
string extension = Path.GetExtension(file).TrimStart('.');
Expand Down
40 changes: 40 additions & 0 deletions SabreTools.Data.Models/GCZ/Archive.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
namespace SabreTools.Data.Models.GCZ
{
/// <summary>
/// Represents a parsed GCZ (GameCube Zip) compressed disc image.
/// Contains header metadata and block lookup tables.
/// Actual compressed block data is accessed via the source stream.
/// </summary>
public class DiscImage
{
/// <summary>
/// GCZ file header
/// </summary>
public GczHeader Header { get; set; } = new();

/// <summary>
/// Block pointer table (one entry per block).
/// Each value encodes both the offset of the block within the compressed data section
/// and a compression flag in the top bit:
/// <list type="bullet">
/// <item>Top bit CLEAR → block is zlib/deflate-compressed at that offset.</item>
/// <item>Top bit SET → block is stored uncompressed at that offset.</item>
/// </list>
/// Offset is <c>value &amp; ~UncompressedFlag</c>.
/// </summary>
public ulong[] BlockPointers { get; set; } = [];

/// <summary>
/// Adler-32 (stored as CRC32) hashes of the uncompressed block data,
Comment thread
Dimensional marked this conversation as resolved.
/// one per block. Used for integrity verification.
/// </summary>
public uint[] BlockHashes { get; set; } = [];

/// <summary>
/// Byte offset within the GCZ file where the compressed block data begins.
/// Computed as: <c>HeaderSize + (NumBlocks * 8) + (NumBlocks * 4)</c>.
/// </summary>
/// <remarks>Not parsed from stream; computed during deserialization.</remarks>
public long DataOffset { get; set; }
Comment thread
Dimensional marked this conversation as resolved.
}
}
23 changes: 23 additions & 0 deletions SabreTools.Data.Models/GCZ/Constants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace SabreTools.Data.Models.GCZ
{
public static class Constants
{
/// <summary>GCZ magic cookie (little-endian u32 at offset 0)</summary>
public const uint MagicCookie = 0xB10BC001;
Comment thread
Dimensional marked this conversation as resolved.

/// <summary>Size of the GCZ file header in bytes</summary>
public const int HeaderSize = 32;

// Valid GCZ block sizes (Dolphin-compatible)
public const uint BlockSize32K = 0x8000;
public const uint BlockSize64K = 0x10000;
public const uint BlockSize128K = 0x20000;
public const uint DefaultBlockSize = BlockSize32K;

/// <summary>
/// Top bit of a block-pointer value: when CLEAR the block is zlib/deflate compressed;
/// when SET the block is stored uncompressed.
/// </summary>
public const ulong UncompressedFlag = 0x8000000000000000;
}
}
39 changes: 39 additions & 0 deletions SabreTools.Data.Models/GCZ/GczHeader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
namespace SabreTools.Data.Models.GCZ
{
/// <summary>
/// GCZ (GameCube Zip) file header — 32 bytes at the start of the file
/// </summary>
/// <see href="https://github.com/dolphin-emu/dolphin/blob/master/Source/Core/DiscIO/CompressedBlob.h"/>
public sealed class GczHeader
{
/// <summary>
/// Magic cookie identifying a GCZ file (0xB10BC001)
/// </summary>
public uint MagicCookie { get; set; }

/// <summary>
/// Sub-type; always 0 for GameCube / Wii disc images
/// </summary>
public uint SubType { get; set; }

/// <summary>
/// Total size of the compressed block data section in bytes
/// </summary>
public ulong CompressedDataSize { get; set; }

/// <summary>
/// Total decompressed (ISO) size in bytes
/// </summary>
public ulong DataSize { get; set; }

/// <summary>
/// Size of each uncompressed block in bytes (must be 32 KiB, 64 KiB, or 128 KiB)
/// </summary>
public uint BlockSize { get; set; }

/// <summary>
/// Number of blocks in the image
/// </summary>
public uint NumBlocks { get; set; }
}
}
120 changes: 120 additions & 0 deletions SabreTools.Data.Models/NintendoDisc/Constants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
namespace SabreTools.Data.Models.NintendoDisc
{
public static class Constants
{
#region Disc identification magic values

/// <summary>Magic word present at offset 0x01C on GameCube discs</summary>
public const uint GCMagicWord = 0xC2339F3D;

/// <summary>Magic word present at offset 0x018 on Wii discs</summary>
public const uint WiiMagicWord = 0x5D1C9EA3;

#endregion

#region Disc header layout

// Offsets within the 0x440-byte boot block.
// Layout confirmed against Dolphin source (VolumeDisc.cpp / DiscUtils.h):
// 0x000–0x003 Title code (4 chars, e.g. "GAFE")
// 0x004–0x005 Maker code (2 chars, e.g. "01") — Dolphin Read(0x4, 2)
// 0x006 Disc number — Dolphin GetDiscNumber() Read(6)
// 0x007 Revision — Dolphin GetRevision() Read(7)
// 0x008 Audio streaming
// 0x009 Streaming buffer size
// 0x00A–0x017 Unused (14 bytes)
// 0x018 Wii magic (0x5D1C9EA3)
// 0x01C GC magic (0xC2339F3D)
// 0x020–0x07F Game title (0x60 bytes)
// 0x080 Disable hash verification
// 0x081 Disable disc encryption
public const int TitleCodeOffset = 0x000;
public const int TitleCodeLength = 4;
public const int MakerCodeOffset = 0x004;
public const int MakerCodeLength = 2;
/// <summary>Full 6-char game ID = TitleCode[4] + MakerCode[2]</summary>
public const int GameIdOffset = 0x000;
public const int GameIdLength = 6;
public const int DiscNumberOffset = 0x006;
public const int DiscVersionOffset = 0x007;
public const int AudioStreamingOffset = 0x008;
public const int StreamingBufferSizeOffset = 0x009;
public const int WiiMagicOffset = 0x018;
public const int GCMagicOffset = 0x01C;
public const int GameTitleOffset = 0x020;
public const int GameTitleLength = 0x060;
public const int DisableHashVerificationOffset = 0x080;
public const int DisableDiscEncryptionOffset = 0x081;
public const int DolOffsetField = 0x420;
public const int FstOffsetField = 0x424;
public const int FstSizeField = 0x428;
public const int DiscHeaderSize = 0x440;

#endregion

#region BI2 data

public const int Bi2Address = 0x000440;
public const int Bi2Size = 0x2000;

#endregion

#region Apploader

public const int ApploaderAddress = 0x002440;
public const int ApploaderCodeSizeOffset = 0x14;
public const int ApploaderTrailerSizeOffset = 0x18;
public const int ApploaderHeaderSize = 0x20;

#endregion

#region Wii-specific disc layout

public const int WiiPartitionTableAddress = 0x40000;
public const int WiiPartitionGroupCount = 4;
public const int WiiRegionDataAddress = 0x04E000;
public const int WiiRegionDataSize = 0x20;

#endregion

#region Wii partition header fields

// Offsets relative to partition start
public const int WiiTicketSize = 0x2A4;
public const int WiiTmdSizeAddress = 0x2A4;
public const int WiiTmdOffsetAddress = 0x2A8;
public const int WiiCertSizeAddress = 0x2AC;
public const int WiiCertOffsetAddress = 0x2B0;
public const int WiiH3OffsetAddress = 0x2B4;
public const int WiiH3Size = 0x18000;
public const int WiiDataOffsetAddress = 0x2B8;

#endregion

#region Wii block / group structure

public const int WiiBlockSize = 0x8000;
public const int WiiBlockHeaderSize = 0x0400;
public const int WiiBlockDataSize = 0x7C00;
public const int WiiBlocksPerGroup = 64;
public const int WiiGroupSize = WiiBlocksPerGroup * WiiBlockSize;
public const int WiiGroupDataSize = WiiBlocksPerGroup * WiiBlockDataSize;

#endregion

#region DVD sector size

public const int DvdSectorSize = 0x800;

#endregion

#region Wii ticket fields

// Offsets relative to ticket start
public const int TicketEncryptedTitleKeyOffset = 0x1BF;
public const int TicketTitleIdOffset = 0x1DC;
public const int TicketCommonKeyIndexOffset = 0x1F1;

#endregion
}
}
28 changes: 28 additions & 0 deletions SabreTools.Data.Models/NintendoDisc/Disc.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
namespace SabreTools.Data.Models.NintendoDisc
{
/// <summary>
/// Represents a parsed GameCube or Wii disc image
/// </summary>
public class Disc
Comment thread
mnadareski marked this conversation as resolved.
{
/// <summary>
/// Disc boot block header (first 0x440 bytes)
/// </summary>
public DiscHeader Header { get; set; } = new();

/// <summary>
/// Detected platform (GameCube or Wii)
/// </summary>
public Platform Platform { get; set; }

/// <summary>
/// Wii partition table entries (Wii discs only)
/// </summary>
public WiiPartitionTableEntry[]? PartitionTableEntries { get; set; }

/// <summary>
/// Wii region data at disc offset 0x4E000 (Wii discs only)
/// </summary>
public WiiRegionData? RegionData { get; set; }
}
}
81 changes: 81 additions & 0 deletions SabreTools.Data.Models/NintendoDisc/DiscHeader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
namespace SabreTools.Data.Models.NintendoDisc
{
/// <summary>
/// GameCube / Wii disc boot block header (first 0x440 bytes of the disc)
/// </summary>
/// <see href="https://wiibrew.org/wiki/Wii_disc"/>
public sealed class DiscHeader
{
/// <summary>
/// 6-character ASCII game ID (e.g. "GALE01")
/// </summary>
/// <remarks>6 bytes at offset 0x000</remarks>
public string GameId { get; set; } = string.Empty;

/// <summary>
/// 2-character ASCII maker / publisher code (e.g. "01")
/// </summary>
/// <remarks>Derived from GameId bytes at offset 0x004–0x005; not a separate on-disc field</remarks>
public string MakerCode { get; set; } = string.Empty;

/// <summary>
/// Zero-based disc number for multi-disc games
/// </summary>
public byte DiscNumber { get; set; }

/// <summary>
/// Disc version
/// </summary>
public byte DiscVersion { get; set; }

/// <summary>
/// Non-zero if audio streaming is enabled
/// </summary>
public byte AudioStreaming { get; set; }

/// <summary>
/// Audio streaming buffer size (in 16 KiB units)
/// </summary>
public byte StreamingBufferSize { get; set; }

/// <summary>
/// Wii magic word at offset 0x018 (0x5D1C9EA3 for Wii discs, 0 for GameCube)
/// </summary>
public uint WiiMagic { get; set; }

/// <summary>
/// GameCube magic word at offset 0x01C (0xC2339F3D for GameCube discs)
/// </summary>
public uint GCMagic { get; set; }
Comment thread
Dimensional marked this conversation as resolved.

/// <summary>
/// Null-terminated ASCII game title (up to 0x60 bytes at offset 0x020)
/// </summary>
public string GameTitle { get; set; } = string.Empty;

/// <summary>
/// Non-zero to disable hash verification (GameCube only)
/// </summary>
public byte DisableHashVerification { get; set; }

/// <summary>
/// Non-zero to disable disc encryption (GameCube only)
/// </summary>
public byte DisableDiscEncryption { get; set; }

/// <summary>
/// Offset of the main DOL executable (no shift for GameCube; &lt;&lt;2 for Wii)
/// </summary>
public uint DolOffset { get; set; }

/// <summary>
/// Offset of the File System Table (no shift for GameCube; &lt;&lt;2 for Wii)
/// </summary>
public uint FstOffset { get; set; }

/// <summary>
/// Maximum size of the File System Table in bytes
/// </summary>
public uint FstSize { get; set; }
}
}
32 changes: 32 additions & 0 deletions SabreTools.Data.Models/NintendoDisc/Enums.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
namespace SabreTools.Data.Models.NintendoDisc
{
/// <summary>
/// Platform / console type for a Nintendo disc image
/// </summary>
public enum Platform
{
/// <summary>Platform could not be determined</summary>
Unknown = 0,

/// <summary>Nintendo GameCube</summary>
GameCube = 1,

/// <summary>Nintendo Wii</summary>
Wii = 2,
}

/// <summary>
/// Wii partition type
/// </summary>
public enum WiiPartitionType : uint
{
/// <summary>Game data partition (DATA)</summary>
Data = 0,

/// <summary>System update partition (UPDATE)</summary>
Update = 1,

/// <summary>Channel installer partition (CHANNEL)</summary>
Channel = 2,
}
}
Loading