-
Notifications
You must be signed in to change notification settings - Fork 4
Dolphin lib #85
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Dolphin lib #85
Changes from all commits
d944d50
2915d24
d598a7e
c96fa2b
242c0f7
53b9366
c620499
a6d1093
d85e97c
51d8a87
5217f41
a9cc96b
9cd62d9
c6f477e
9667392
05bf4ed
095d6ef
a014f5e
42ff5eb
08fc523
a30e32d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| using System.IO; | ||
| using SabreTools.Data.Models.GCZ; | ||
| using SabreTools.IO.Extensions; | ||
| using SabreTools.Numerics.Extensions; | ||
|
|
||
| #pragma warning disable IDE0017 // Simplify object initialization | ||
| namespace SabreTools.Serialization.Readers | ||
| { | ||
| public class GCZ : BaseBinaryReader<DiscImage> | ||
| { | ||
| /// <inheritdoc/> | ||
| public override DiscImage? Deserialize(Stream? data) | ||
| { | ||
| // If the data is invalid | ||
| if (data is null || !data.CanRead) | ||
| return null; | ||
|
|
||
| // Need at least the header | ||
| if (data.Length - data.Position < Constants.HeaderSize) | ||
| return null; | ||
|
|
||
| try | ||
| { | ||
| long initialOffset = data.Position; | ||
|
|
||
| var archive = new DiscImage(); | ||
|
|
||
| // Parse the header | ||
| archive.Header = ParseGczHeader(data); | ||
| if (archive.Header.MagicCookie != Constants.MagicCookie) | ||
| return null; | ||
|
|
||
| // Validate block count — guard against absurdly large tables | ||
| if (archive.Header.NumBlocks == 0 || archive.Header.NumBlocks > 0x100000) | ||
| return null; | ||
|
|
||
| int numBlocks = (int)archive.Header.NumBlocks; | ||
|
|
||
| // Read block pointer table (8 bytes per block) | ||
| archive.BlockPointers = new ulong[numBlocks]; | ||
| byte[] ptrBuf = data.ReadBytes(numBlocks * 8); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can take advantage of my |
||
| for (int i = 0; i < numBlocks; i++) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Formatting nitpick: This applies to all |
||
| archive.BlockPointers[i] = System.BitConverter.ToUInt64(ptrBuf, i * 8); | ||
|
|
||
| // Read block hash table (4 bytes per block, Adler-32) | ||
| archive.BlockHashes = new uint[numBlocks]; | ||
| byte[] hashBuf = data.ReadBytes(numBlocks * 4); | ||
| for (int i = 0; i < numBlocks; i++) | ||
| archive.BlockHashes[i] = System.BitConverter.ToUInt32(hashBuf, i * 4); | ||
|
|
||
| return archive; | ||
| } | ||
| catch | ||
| { | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| private static GczHeader ParseGczHeader(Stream data) | ||
| { | ||
| var header = new GczHeader(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nitpicks that apply to multiple places, I just picked the first place:
|
||
| header.MagicCookie = data.ReadUInt32LittleEndian(); | ||
| header.SubType = data.ReadUInt32LittleEndian(); | ||
| header.CompressedDataSize = data.ReadUInt64LittleEndian(); | ||
| header.DataSize = data.ReadUInt64LittleEndian(); | ||
| header.BlockSize = data.ReadUInt32LittleEndian(); | ||
| header.NumBlocks = data.ReadUInt32LittleEndian(); | ||
| return header; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,205 @@ | ||||||
| using System.IO; | ||||||
| using System.Text; | ||||||
| using SabreTools.Data.Models.NintendoDisc; | ||||||
| using SabreTools.IO.Extensions; | ||||||
| using SabreTools.Numerics.Extensions; | ||||||
|
|
||||||
| #pragma warning disable IDE0017 // Simplify object initialization | ||||||
| namespace SabreTools.Serialization.Readers | ||||||
| { | ||||||
| public class NintendoDisc : BaseBinaryReader<Disc> | ||||||
| { | ||||||
| /// <inheritdoc/> | ||||||
| public override Disc? Deserialize(Stream? data) | ||||||
| { | ||||||
| // If the data is invalid | ||||||
| if (data is null || !data.CanRead) | ||||||
| return null; | ||||||
|
|
||||||
| // Need at least the disc header | ||||||
| if (data.Length - data.Position < Constants.DiscHeaderSize) | ||||||
| return null; | ||||||
|
|
||||||
| try | ||||||
| { | ||||||
| long initialOffset = data.Position; | ||||||
|
|
||||||
| var disc = new Disc(); | ||||||
|
|
||||||
| // Parse the disc header | ||||||
| disc.Header = ParseDiscHeader(data); | ||||||
|
|
||||||
| // 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) | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to GCZ, I mentioned in the models that this feels like a good thing to have as an extension property. With that said, I can see the utility for parsing here, so don't feel like you need to remove this logic from here entirely. |
||||||
| 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; | ||||||
|
|
||||||
| // Parse Wii-specific structures | ||||||
| if (disc.Platform == Platform.Wii) | ||||||
| { | ||||||
| // Partition table starts at 0x40000 | ||||||
| long partTableEnd = initialOffset + Constants.WiiPartitionTableAddress | ||||||
| + (Constants.WiiPartitionGroupCount * 8); | ||||||
| if (data.Length >= partTableEnd) | ||||||
| disc.PartitionTableEntries = ParsePartitionTable(data, initialOffset); | ||||||
|
|
||||||
| // Region data at 0x4E000 | ||||||
| long regionEnd = initialOffset + Constants.WiiRegionDataAddress + Constants.WiiRegionDataSize; | ||||||
| if (data.Length >= regionEnd) | ||||||
| { | ||||||
| data.Seek(initialOffset + Constants.WiiRegionDataAddress, SeekOrigin.Begin); | ||||||
| disc.RegionData = ParseRegionData(data); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| return disc; | ||||||
| } | ||||||
| catch | ||||||
| { | ||||||
| return null; | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| #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. | ||||||
| /// </summary> | ||||||
| public static DiscHeader? ParseDiscHeaderOnly(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(); | ||||||
|
|
||||||
| // 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'); | ||||||
|
|
||||||
| // Maker code is the last 2 chars of the GameId (offsets 0x004–0x005). | ||||||
| // 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; | ||||||
|
|
||||||
| // 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(); | ||||||
|
|
||||||
| // Skip unused 0x0E bytes (offsets 0x00A–0x017) | ||||||
| data.ReadBytes(0x0E); | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pedantic: Padding and "unused" bytes tend to be read as well in the majority of situations. It may not be useful, but it provides a more accurate picture. This applies to any other places where you're using "empty" reads. |
||||||
|
|
||||||
| header.WiiMagic = data.ReadUInt32BigEndian(); | ||||||
| header.GCMagic = data.ReadUInt32BigEndian(); | ||||||
|
Comment on lines
+110
to
+111
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This has me a bit confused. Are there actually two distinct fields for Wii and GC magic values in the data? Or are these meant to be a single property that could be either the Wii or GC magic value? |
||||||
|
|
||||||
| 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; | ||||||
| } | ||||||
|
|
||||||
| #endregion | ||||||
|
|
||||||
| #region Wii partition table parsing | ||||||
|
|
||||||
| private static WiiPartitionTableEntry[]? ParsePartitionTable(Stream data, long baseOffset) | ||||||
| { | ||||||
| data.Seek(baseOffset + Constants.WiiPartitionTableAddress, SeekOrigin.Begin); | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Generally the parsing methods shouldn't have to be aware of moving to their own start locations. I would prefer if you do the seek outside of this method and let this code assume it's at the correct spot. |
||||||
|
|
||||||
| // Read 4 partition groups; each group has a count and a shifted offset | ||||||
| var allEntries = new System.Collections.Generic.List<WiiPartitionTableEntry>(); | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Even if you're only using things like |
||||||
|
|
||||||
| for (int g = 0; g < Constants.WiiPartitionGroupCount; g++) | ||||||
| { | ||||||
| uint count = data.ReadUInt32BigEndian(); | ||||||
| uint shiftedOffset = data.ReadUInt32BigEndian(); | ||||||
|
|
||||||
| if (count == 0) | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this check be done directly after you read the count? Or are there always guaranteed to be the count and offset fields? |
||||||
| 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(); | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nitpick: I used to do this as well where "nested" reads were just done directly. I currently prefer having separate Parse methods, even for these cases. |
||||||
| 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; | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nitpick: I prefer the newer syntax for transforming collections
Suggested change
|
||||||
| } | ||||||
|
|
||||||
| #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. | ||||||
| /// </summary> | ||||||
| private static bool IsGameCubeTitleType(char c) | ||||||
| { | ||||||
| return c == 'G' || c == 'D' || c == 'R'; | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are large tables valid? Or is there a known cutoff? I see you're using
0x100000so knowing where that comes from would be neat.