Start adding NGCW dumping

This commit is contained in:
Rebecca Wallander
2026-03-24 21:30:25 +01:00
parent 491a56c105
commit c1f703d8e7
15 changed files with 936 additions and 132 deletions

View File

@@ -1,4 +1,4 @@
// /***************************************************************************
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
@@ -348,10 +348,7 @@ partial class Dump
if(nintendoPfi is { DiskCategory: DiskCategory.Nintendo, PartVersion: 15 })
{
StoppingErrorMessage?.Invoke(Localization.Core
.Dumping_Nintendo_GameCube_or_Wii_discs_is_not_yet_implemented);
return;
dskType = nintendoPfi.Value.DiscSize == DVDSize.Eighty ? MediaType.GOD : MediaType.WOD;
}
}

View File

@@ -35,6 +35,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http.Headers;
using System.Text.Json;
using Aaru.CommonTypes;
using Aaru.CommonTypes.AaruMetadata;
@@ -337,6 +338,30 @@ partial class Dump
}
}
bool ngcwMode = dskType is MediaType.GOD or MediaType.WOD;
if(ngcwMode)
{
if(!scsiReader.OmniDriveReadRaw)
{
StoppingErrorMessage?.Invoke(Localization.Core.Dumping_Nintendo_GameCube_or_Wii_discs_is_not_yet_implemented);
return;
}
if(outputFormat.Format != "Aaru")
{
StoppingErrorMessage?.Invoke(string.Format(Localization.Core.Output_format_does_not_support_0,
MediaTagType.NgcwJunkMap));
return;
}
if(blocksToRead > 16) blocksToRead = 16;
}
scsiReader.OmniDriveNintendoMode = ngcwMode;
ret = true;
foreach(MediaTagType tag in mediaTags.Keys.Where(tag => !outputFormat.SupportedMediaTags.Contains(tag)))
@@ -406,7 +431,9 @@ partial class Dump
((dskType >= MediaType.DVDROM && dskType <= MediaType.DVDDownload)
|| dskType == MediaType.PS2DVD
|| dskType == MediaType.PS3DVD
|| dskType == MediaType.Nuon))
|| dskType == MediaType.Nuon
|| dskType == MediaType.GOD
|| dskType == MediaType.WOD))
nominalNegativeSectors = Math.Min(nominalNegativeSectors, DvdLeadinSectors);
mediaTags.TryGetValue(MediaTagType.BD_DI, out byte[] di);
@@ -783,6 +810,9 @@ partial class Dump
var newTrim = false;
if(ngcwMode && !InitializeNgcwContext(dskType, scsiReader, outputFormat))
return;
if(mediaTags.TryGetValue(MediaTagType.DVD_CMI, out byte[] cmi) &&
Settings.Settings.Current.EnableDecryption &&
_titleKeys &&
@@ -938,6 +968,8 @@ partial class Dump
#endregion Error handling
if(ngcwMode) FinalizeNgcwContext(outputFormat);
if(opticalDisc)
{
foreach(KeyValuePair<MediaTagType, byte[]> tag in mediaTags)

View File

@@ -308,46 +308,56 @@ partial class Dump
if(scsiReader.ReadBuffer3CReadRaw || scsiReader.OmniDriveReadRaw || scsiReader.HldtstReadRaw)
{
var cmi = new byte[1];
byte[] key = buffer.Skip(7).Take(5).ToArray();
if(key.All(static k => k == 0))
if(_ngcwEnabled)
{
outputFormat.WriteSectorTag(new byte[5], badSector, false, SectorTagType.DvdTitleKeyDecrypted);
MarkTitleKeyDumped(badSector);
if(TransformNgcwLongSectors(scsiReader, buffer, badSector, 1, out SectorStatus[] statuses))
outputFormat.WriteSectorLong(buffer, badSector, false, statuses[0]);
else
outputFormat.WriteSectorLong(buffer, badSector, false, SectorStatus.NotDumped);
}
else
{
CSS.DecryptTitleKey(discKey, key, out byte[] tmpBuf);
outputFormat.WriteSectorTag(tmpBuf, badSector, false, SectorTagType.DvdTitleKeyDecrypted);
MarkTitleKeyDumped(badSector);
var cmi = new byte[1];
cmi[0] = buffer[6];
}
byte[] key = buffer.Skip(7).Take(5).ToArray();
if(!_storeEncrypted)
{
ErrorNumber errno =
outputFormat.ReadSectorsTag(badSector,
false,
1,
SectorTagType.DvdTitleKeyDecrypted,
out byte[] titleKey);
if(errno != ErrorNumber.NoError)
if(key.All(static k => k == 0))
{
ErrorMessage?.Invoke(string.Format(Localization.Core
.Error_retrieving_title_key_for_sector_0,
badSector));
outputFormat.WriteSectorTag(new byte[5], badSector, false, SectorTagType.DvdTitleKeyDecrypted);
MarkTitleKeyDumped(badSector);
}
else
buffer = CSS.DecryptSectorLong(buffer, titleKey, cmi);
}
{
CSS.DecryptTitleKey(discKey, key, out byte[] tmpBuf);
outputFormat.WriteSectorTag(tmpBuf, badSector, false, SectorTagType.DvdTitleKeyDecrypted);
MarkTitleKeyDumped(badSector);
_resume.BadBlocks.Remove(badSector);
outputFormat.WriteSectorLong(buffer, badSector, false, SectorStatus.Dumped);
cmi[0] = buffer[6];
}
if(!_storeEncrypted)
{
ErrorNumber errno =
outputFormat.ReadSectorsTag(badSector,
false,
1,
SectorTagType.DvdTitleKeyDecrypted,
out byte[] titleKey);
if(errno != ErrorNumber.NoError)
{
ErrorMessage?.Invoke(string.Format(Localization.Core
.Error_retrieving_title_key_for_sector_0,
badSector));
}
else
buffer = CSS.DecryptSectorLong(buffer, titleKey, cmi);
}
_resume.BadBlocks.Remove(badSector);
outputFormat.WriteSectorLong(buffer, badSector, false, SectorStatus.Dumped);
}
}
else
outputFormat.WriteSector(buffer, badSector, false, SectorStatus.Dumped);

View File

@@ -0,0 +1,606 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Ngcw.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// --[ Description ] ----------------------------------------------------------
//
// NGCW (GameCube/Wii) helpers for OmniDrive raw DVD dumping.
//
// --[ License ] --------------------------------------------------------------
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2026 Natalia Portillo
// Copyright © 2020-2026 Rebecca Wallander
// ****************************************************************************/
using System;
using System.Collections.Generic;
using System.Linq;
using Aaru.CommonTypes;
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Interfaces;
using Aaru.Core.Image.Ngcw;
using Aaru.Decoders.Nintendo;
using Aaru.Helpers;
using Aaru.Localization;
using NgcwPartitions = Aaru.Core.Image.Ngcw.Partitions;
namespace Aaru.Core.Devices.Dumping;
partial class Dump
{
const int NGCW_LONG_SECTOR_SIZE = 2064;
const int NGCW_PAYLOAD_OFFSET = 6;
const int NGCW_SECTOR_SIZE = 2048;
const int NGCW_SECTORS_PER_GROUP = 16;
bool _ngcwEnabled;
MediaType _ngcwMediaType;
JunkCollector _ngcwJunkCollector;
List<WiiPartition> _ngcwPartitions;
DataRegion[][] _ngcwPartDataMaps;
ulong[] _ngcwPartSysEnd;
DataRegion[] _ngcwGcDataMap;
ulong _ngcwGcSysEnd;
bool _omniDriveNintendoSoftwareDescramble;
byte? _nintendoDerivedDiscKey;
readonly Aaru.Decoders.Nintendo.Sector _nintendoSectorDecoder = new Aaru.Decoders.Nintendo.Sector();
bool InitializeNgcwContext(MediaType dskType, Reader scsiReader, IWritableImage outputFormat)
{
_ngcwEnabled = dskType is MediaType.GOD or MediaType.WOD;
_ngcwMediaType = dskType;
_ngcwJunkCollector = new JunkCollector();
_omniDriveNintendoSoftwareDescramble = scsiReader.OmniDriveNintendoMode;
if(!_ngcwEnabled) return true;
if(_omniDriveNintendoSoftwareDescramble)
{
UpdateStatus?.Invoke(UI.Ngcw_nintendo_software_descramble);
if(!EnsureNintendoDerivedKeyFromLba0(scsiReader)) return false;
}
if(dskType == MediaType.GOD)
return InitializeGameCubeContext(scsiReader);
return InitializeWiiContext(scsiReader, outputFormat);
}
void FinalizeNgcwContext(IWritableImage outputFormat)
{
if(!_ngcwEnabled || _ngcwJunkCollector is null || _ngcwJunkCollector.Count == 0) return;
byte[] junkMapData = Junk.Serialize(_ngcwJunkCollector.Entries);
outputFormat.WriteMediaTag(junkMapData, MediaTagType.NgcwJunkMap);
UpdateStatus?.Invoke(string.Format(UI.Ngcw_stored_junk_map_0_entries_1_bytes,
_ngcwJunkCollector.Count,
junkMapData.Length));
}
bool TransformNgcwLongSectors(Reader scsiReader, byte[] longBuffer, ulong startSector, uint sectors,
out SectorStatus[] statuses)
{
statuses = new SectorStatus[sectors];
if(!_ngcwEnabled)
{
for(int i = 0; i < sectors; i++) statuses[i] = SectorStatus.Dumped;
return true;
}
if(_omniDriveNintendoSoftwareDescramble)
DescrambleNintendoLongBuffer(longBuffer, startSector, sectors);
if(_ngcwMediaType == MediaType.GOD)
return TransformGameCubeLongSectors(longBuffer, startSector, sectors, statuses);
return TransformWiiLongSectors(scsiReader, longBuffer, startSector, sectors, statuses);
}
bool InitializeGameCubeContext(Reader scsiReader)
{
byte[] extHeader = ReadDiscBytesFromDevice(scsiReader, 0, 0x440);
if(extHeader == null || extHeader.Length < 0x42C)
{
StoppingErrorMessage?.Invoke(Localization.Core.Unable_to_read_medium);
return false;
}
uint fstOffset = BigEndianBitConverter.ToUInt32(extHeader, 0x424);
uint fstSize = BigEndianBitConverter.ToUInt32(extHeader, 0x428);
_ngcwGcSysEnd = fstOffset + fstSize;
if(fstSize > 0 && fstSize < 64 * 1024 * 1024)
{
byte[] fst = ReadDiscBytesFromDevice(scsiReader, fstOffset, (int)fstSize);
if(fst != null) _ngcwGcDataMap = DataMap.BuildFromFst(fst, 0, 0);
}
return true;
}
bool InitializeWiiContext(Reader scsiReader, IWritableImage outputFormat)
{
UpdateStatus?.Invoke(UI.Ngcw_parsing_partition_table);
_ngcwPartitions = ParseWiiPartitionsFromDevice(scsiReader);
if(_ngcwPartitions == null)
{
StoppingErrorMessage?.Invoke(Localization.Core.Unable_to_read_medium);
return false;
}
WiiPartitionRegion[] regions = NgcwPartitions.BuildRegionMap(_ngcwPartitions);
byte[] keyMapData = NgcwPartitions.SerializeKeyMap(regions);
outputFormat.WriteMediaTag(keyMapData, MediaTagType.WiiPartitionKeyMap);
UpdateStatus?.Invoke(UI.Ngcw_written_partition_key_map);
BuildWiiPartitionFstMaps(scsiReader);
return true;
}
void BuildWiiPartitionFstMaps(Reader scsiReader)
{
if(_ngcwPartitions == null || _ngcwPartitions.Count == 0) return;
_ngcwPartDataMaps = new DataRegion[_ngcwPartitions.Count][];
_ngcwPartSysEnd = new ulong[_ngcwPartitions.Count];
for(int p = 0; p < _ngcwPartitions.Count; p++)
{
byte[] encGrp0 = ReadRawPayloadSectors(scsiReader, _ngcwPartitions[p].DataOffset / NGCW_SECTOR_SIZE, 16);
if(encGrp0 == null || encGrp0.Length < Crypto.GROUP_SIZE) continue;
byte[] hb0 = new byte[Crypto.GROUP_HASH_SIZE];
byte[] gd0 = new byte[Crypto.GROUP_DATA_SIZE];
Crypto.DecryptGroup(_ngcwPartitions[p].TitleKey, encGrp0, hb0, gd0);
uint fstOffset = BigEndianBitConverter.ToUInt32(gd0, 0x424) << 2;
uint fstSize = BigEndianBitConverter.ToUInt32(gd0, 0x428) << 2;
_ngcwPartSysEnd[p] = fstOffset + fstSize;
if(fstSize == 0 || fstSize >= 64 * 1024 * 1024) continue;
byte[] fstBuffer = new byte[fstSize];
uint fstRead = 0;
bool ok = true;
while(fstRead < fstSize)
{
ulong logicalOffset = fstOffset + fstRead;
ulong groupIndex = logicalOffset / Crypto.GROUP_DATA_SIZE;
int groupOffset = (int)(logicalOffset % Crypto.GROUP_DATA_SIZE);
ulong discOffset = _ngcwPartitions[p].DataOffset + groupIndex * Crypto.GROUP_SIZE;
byte[] encGroup = ReadRawPayloadSectors(scsiReader, discOffset / NGCW_SECTOR_SIZE, 16);
if(encGroup == null || encGroup.Length < Crypto.GROUP_SIZE)
{
ok = false;
break;
}
byte[] hb = new byte[Crypto.GROUP_HASH_SIZE];
byte[] gd = new byte[Crypto.GROUP_DATA_SIZE];
Crypto.DecryptGroup(_ngcwPartitions[p].TitleKey, encGroup, hb, gd);
int available = Crypto.GROUP_DATA_SIZE - groupOffset;
int chunk = fstSize - (int)fstRead < available ? (int)(fstSize - fstRead) : available;
Array.Copy(gd, groupOffset, fstBuffer, fstRead, chunk);
fstRead += (uint)chunk;
}
if(ok) _ngcwPartDataMaps[p] = DataMap.BuildFromFst(fstBuffer, 0, 2);
}
}
bool TransformGameCubeLongSectors(byte[] longBuffer, ulong startSector, uint sectors, SectorStatus[] statuses)
{
byte[] payload = new byte[sectors * NGCW_SECTOR_SIZE];
for(uint i = 0; i < sectors; i++)
Array.Copy(longBuffer, i * NGCW_LONG_SECTOR_SIZE + NGCW_PAYLOAD_OFFSET, payload, i * NGCW_SECTOR_SIZE,
NGCW_SECTOR_SIZE);
ulong dataSectors = 0;
ulong junkSectors = 0;
Junk.DetectJunkInBlock(payload,
payload.Length,
startSector * NGCW_SECTOR_SIZE,
_ngcwGcDataMap,
_ngcwGcSysEnd,
0xFFFF,
_ngcwJunkCollector,
ref dataSectors,
ref junkSectors,
statuses);
return true;
}
bool TransformWiiLongSectors(Reader scsiReader, byte[] longBuffer, ulong startSector, uint sectors, SectorStatus[] statuses)
{
ulong discOffset = startSector * NGCW_SECTOR_SIZE;
int partIndex = NgcwPartitions.FindPartitionAtOffset(_ngcwPartitions, discOffset);
if(partIndex < 0)
{
byte[] payload = new byte[sectors * NGCW_SECTOR_SIZE];
for(uint i = 0; i < sectors; i++)
Array.Copy(longBuffer, i * NGCW_LONG_SECTOR_SIZE + NGCW_PAYLOAD_OFFSET, payload, i * NGCW_SECTOR_SIZE,
NGCW_SECTOR_SIZE);
ulong dataSectors = 0;
ulong junkSectors = 0;
Junk.DetectJunkInBlock(payload,
payload.Length,
discOffset,
null,
0x50000,
0xFFFF,
_ngcwJunkCollector,
ref dataSectors,
ref junkSectors,
statuses);
return true;
}
ulong groupStartOffset = _ngcwPartitions[partIndex].DataOffset +
(discOffset - _ngcwPartitions[partIndex].DataOffset) / Crypto.GROUP_SIZE * Crypto.GROUP_SIZE;
byte[] groupPayload;
if(sectors == NGCW_SECTORS_PER_GROUP && discOffset == groupStartOffset)
{
groupPayload = new byte[Crypto.GROUP_SIZE];
for(uint i = 0; i < sectors; i++)
Array.Copy(longBuffer, i * NGCW_LONG_SECTOR_SIZE + NGCW_PAYLOAD_OFFSET, groupPayload, i * NGCW_SECTOR_SIZE,
NGCW_SECTOR_SIZE);
}
else
{
groupPayload = ReadRawPayloadSectors(scsiReader, groupStartOffset / NGCW_SECTOR_SIZE, NGCW_SECTORS_PER_GROUP);
if(groupPayload == null || groupPayload.Length < Crypto.GROUP_SIZE) return false;
}
byte[] hashBlock = new byte[Crypto.GROUP_HASH_SIZE];
byte[] groupData = new byte[Crypto.GROUP_DATA_SIZE];
Crypto.DecryptGroup(_ngcwPartitions[partIndex].TitleKey, groupPayload, hashBlock, groupData);
ulong groupNumber = (groupStartOffset - _ngcwPartitions[partIndex].DataOffset) / Crypto.GROUP_SIZE;
ulong logicalOffset = groupNumber * Crypto.GROUP_DATA_SIZE;
bool[] sectorIsData = new bool[NGCW_SECTORS_PER_GROUP];
int userDataCount = 0;
for(ulong off = 0; off < Crypto.GROUP_DATA_SIZE; off += NGCW_SECTOR_SIZE)
{
ulong chunk = Crypto.GROUP_DATA_SIZE - off;
if(chunk > NGCW_SECTOR_SIZE) chunk = NGCW_SECTOR_SIZE;
if(logicalOffset + off < _ngcwPartSysEnd[partIndex])
sectorIsData[userDataCount] = true;
else if(_ngcwPartDataMaps[partIndex] != null)
{
sectorIsData[userDataCount] = DataMap.IsDataRegion(_ngcwPartDataMaps[partIndex], logicalOffset + off, chunk);
}
else
sectorIsData[userDataCount] = true;
userDataCount++;
}
ulong blockPhase = logicalOffset % Crypto.GROUP_SIZE;
ulong block2Start = blockPhase > 0 ? Crypto.GROUP_SIZE - blockPhase : Crypto.GROUP_DATA_SIZE;
if(block2Start > Crypto.GROUP_DATA_SIZE) block2Start = Crypto.GROUP_DATA_SIZE;
bool haveSeed1 = false;
uint[] seed1 = new uint[Lfg.SEED_SIZE];
bool haveSeed2 = false;
uint[] seed2 = new uint[Lfg.SEED_SIZE];
for(int s = 0; s < userDataCount; s++)
{
if(sectorIsData[s]) continue;
ulong sectorOffset = (ulong)s * NGCW_SECTOR_SIZE;
bool inBlock2 = sectorOffset >= block2Start;
if(inBlock2 && haveSeed2) continue;
if(!inBlock2 && haveSeed1) continue;
int available = Crypto.GROUP_DATA_SIZE - (int)sectorOffset;
int dataOffset = (int)((logicalOffset + sectorOffset) % Crypto.GROUP_SIZE);
if(available < Lfg.MIN_SEED_DATA_BYTES) continue;
uint[] destination = inBlock2 ? seed2 : seed1;
int matched = Lfg.GetSeed(groupData.AsSpan((int)sectorOffset, available), dataOffset, destination);
if(matched > 0)
{
if(inBlock2)
haveSeed2 = true;
else
haveSeed1 = true;
}
if(haveSeed1 && haveSeed2) break;
}
byte[] decryptedGroup = new byte[Crypto.GROUP_SIZE];
Array.Copy(hashBlock, 0, decryptedGroup, 0, Crypto.GROUP_HASH_SIZE);
for(int s = 0; s < userDataCount; s++)
{
ulong off = (ulong)s * NGCW_SECTOR_SIZE;
int chunk = Crypto.GROUP_DATA_SIZE - (int)off;
int outOff = Crypto.GROUP_HASH_SIZE + (int)off;
if(chunk > NGCW_SECTOR_SIZE) chunk = NGCW_SECTOR_SIZE;
if(sectorIsData[s])
{
Array.Copy(groupData, (int)off, decryptedGroup, outOff, chunk);
continue;
}
bool inBlock2 = off >= block2Start;
bool haveSeed = inBlock2 ? haveSeed2 : haveSeed1;
uint[] seed = inBlock2 ? seed2 : seed1;
if(!haveSeed)
{
Array.Copy(groupData, (int)off, decryptedGroup, outOff, chunk);
continue;
}
uint[] lfgBuffer = new uint[Lfg.MIN_SEED_DATA_BYTES / sizeof(uint)];
uint[] seedCopy = new uint[Lfg.SEED_SIZE];
int position = 0;
byte[] expectedData = new byte[NGCW_SECTOR_SIZE];
Array.Copy(seed, seedCopy, Lfg.SEED_SIZE);
Lfg.SetSeed(lfgBuffer, seedCopy);
int advance = (int)((logicalOffset + off) % Crypto.GROUP_SIZE);
if(advance > 0)
{
byte[] discard = new byte[4096];
int remain = advance;
while(remain > 0)
{
int step = remain > discard.Length ? discard.Length : remain;
Lfg.GetBytes(lfgBuffer, ref position, discard, 0, step);
remain -= step;
}
}
Lfg.GetBytes(lfgBuffer, ref position, expectedData, 0, chunk);
if(groupData.AsSpan((int)off, chunk).SequenceEqual(expectedData.AsSpan(0, chunk)))
{
Array.Clear(decryptedGroup, outOff, chunk);
_ngcwJunkCollector.Add(groupStartOffset + Crypto.GROUP_HASH_SIZE + off, (ulong)chunk, (ushort)partIndex, seed);
}
else
Array.Copy(groupData, (int)off, decryptedGroup, outOff, chunk);
}
for(uint i = 0; i < sectors; i++)
{
ulong absoluteSector = startSector + i;
int groupIndex = (int)(absoluteSector - groupStartOffset / NGCW_SECTOR_SIZE);
if(groupIndex < 0 || groupIndex >= NGCW_SECTORS_PER_GROUP) continue;
Array.Copy(decryptedGroup,
groupIndex * NGCW_SECTOR_SIZE,
longBuffer,
i * NGCW_LONG_SECTOR_SIZE + NGCW_PAYLOAD_OFFSET,
NGCW_SECTOR_SIZE);
statuses[i] = SectorStatus.Unencrypted;
}
return true;
}
bool EnsureNintendoDerivedKeyFromLba0(Reader scsiReader)
{
if(!_omniDriveNintendoSoftwareDescramble || _nintendoDerivedDiscKey.HasValue) return true;
bool sense = scsiReader.ReadBlock(out byte[] raw, 0, out _, out _, out _);
if(sense || _dev.Error || raw == null || raw.Length < NGCW_LONG_SECTOR_SIZE)
{
StoppingErrorMessage?.Invoke(Localization.Core.Unable_to_read_medium);
return false;
}
_ = _nintendoSectorDecoder.Scramble(raw, 0, out byte[] descrambled);
if(descrambled == null)
{
StoppingErrorMessage?.Invoke(Localization.Core.Unable_to_read_medium);
return false;
}
byte[] cprMai8 = new byte[8];
Array.Copy(descrambled, 6, cprMai8, 0, 8);
_nintendoDerivedDiscKey = Sector.DeriveNintendoKey(cprMai8);
UpdateStatus?.Invoke(string.Format(UI.Ngcw_nintendo_derived_key_0, _nintendoDerivedDiscKey.Value));
return true;
}
void DescrambleNintendoLongBuffer(byte[] longBuffer, ulong startSector, uint sectors)
{
if(!_omniDriveNintendoSoftwareDescramble) return;
for(uint i = 0; i < sectors; i++)
DescrambleNintendo2064At(longBuffer, (int)(i * NGCW_LONG_SECTOR_SIZE), startSector + i);
}
void DescrambleNintendo2064At(byte[] buffer, int offset, ulong lba)
{
byte[] one = new byte[NGCW_LONG_SECTOR_SIZE];
Array.Copy(buffer, offset, one, 0, NGCW_LONG_SECTOR_SIZE);
byte key = lba < NGCW_SECTORS_PER_GROUP ? (byte)0 : (_nintendoDerivedDiscKey ?? (byte)0);
var error = _nintendoSectorDecoder.Scramble(one, key, out byte[] decoded);
if(error != ErrorNumber.NoError)
return;
if(decoded != null) Array.Copy(decoded, 0, buffer, offset, NGCW_LONG_SECTOR_SIZE);
if(lba == 0 && decoded != null)
{
byte[] cprMai8 = new byte[8];
Array.Copy(decoded, 6, cprMai8, 0, 8);
_nintendoDerivedDiscKey = Sector.DeriveNintendoKey(cprMai8);
}
}
List<WiiPartition> ParseWiiPartitionsFromDevice(Reader scsiReader)
{
byte[] partitionTable = ReadDiscBytesFromDevice(scsiReader, 0x40000, 32);
if(partitionTable == null) return null;
List<WiiPartition> partitions = new List<WiiPartition>();
uint[] counts = new uint[4];
uint[] offsets = new uint[4];
for(int t = 0; t < 4; t++)
{
counts[t] = BigEndianBitConverter.ToUInt32(partitionTable, t * 8);
offsets[t] = BigEndianBitConverter.ToUInt32(partitionTable, t * 8 + 4);
}
for(int t = 0; t < 4; t++)
{
if(counts[t] == 0) continue;
ulong tableOffset = (ulong)offsets[t] << 2;
int tableSize = (int)counts[t] * 8;
byte[] tableData = ReadDiscBytesFromDevice(scsiReader, tableOffset, tableSize);
if(tableData == null) return null;
for(uint p = 0; p < counts[t]; p++)
{
ulong partitionOffset = (ulong)BigEndianBitConverter.ToUInt32(tableData, (int)p * 8) << 2;
uint partType = BigEndianBitConverter.ToUInt32(tableData, (int)p * 8 + 4);
byte[] ticket = ReadDiscBytesFromDevice(scsiReader, partitionOffset, 0x2A4);
if(ticket == null) return null;
byte[] titleKey = Crypto.DecryptTitleKey(ticket);
byte[] header = ReadDiscBytesFromDevice(scsiReader, partitionOffset + 0x2B8, 8);
if(header == null) return null;
ulong dataOffset = partitionOffset + ((ulong)BigEndianBitConverter.ToUInt32(header, 0) << 2);
ulong dataSize = (ulong)BigEndianBitConverter.ToUInt32(header, 4) << 2;
partitions.Add(new WiiPartition
{
Offset = partitionOffset,
DataOffset = dataOffset,
DataSize = dataSize,
Type = partType,
TitleKey = titleKey
});
}
}
return partitions;
}
byte[] ReadDiscBytesFromDevice(Reader scsiReader, ulong byteOffset, int length)
{
byte[] result = new byte[length];
int read = 0;
while(read < length)
{
ulong sector = (byteOffset + (ulong)read) / NGCW_SECTOR_SIZE;
int sectorOff = (int)((byteOffset + (ulong)read) % NGCW_SECTOR_SIZE);
int chunk = NGCW_SECTOR_SIZE - sectorOff;
if(chunk > length - read) chunk = length - read;
bool sense = scsiReader.ReadBlock(out byte[] rawSector, sector, out _, out _, out _);
if(sense || _dev.Error || rawSector == null || rawSector.Length < NGCW_PAYLOAD_OFFSET + NGCW_SECTOR_SIZE)
return null;
if(_omniDriveNintendoSoftwareDescramble && rawSector.Length >= NGCW_LONG_SECTOR_SIZE)
DescrambleNintendo2064At(rawSector, 0, sector);
Array.Copy(rawSector, NGCW_PAYLOAD_OFFSET + sectorOff, result, read, chunk);
read += chunk;
}
return result;
}
byte[] ReadRawPayloadSectors(Reader scsiReader, ulong startSector, uint count)
{
bool sense = scsiReader.ReadBlocks(out byte[] rawBuffer, startSector, count, out _, out _, out _);
if(sense || _dev.Error || rawBuffer == null) return null;
if(_omniDriveNintendoSoftwareDescramble && rawBuffer.Length >= count * NGCW_LONG_SECTOR_SIZE)
{
for(uint i = 0; i < count; i++)
DescrambleNintendo2064At(rawBuffer, (int)(i * NGCW_LONG_SECTOR_SIZE), startSector + i);
}
byte[] payload = new byte[count * NGCW_SECTOR_SIZE];
for(uint i = 0; i < count; i++)
Array.Copy(rawBuffer, i * NGCW_LONG_SECTOR_SIZE + NGCW_PAYLOAD_OFFSET, payload, i * NGCW_SECTOR_SIZE,
NGCW_SECTOR_SIZE);
return payload;
}
}

View File

@@ -209,55 +209,82 @@ partial class Dump
_writeStopwatch.Restart();
byte[] tmpBuf;
var cmi = new byte[blocksToRead];
for(uint j = 0; j < blocksToRead; j++)
if(_ngcwEnabled)
{
byte[] key = buffer.Skip((int)(2064 * j + 7)).Take(5).ToArray();
if(key.All(static k => k == 0))
if(!TransformNgcwLongSectors(scsiReader, buffer, i, blocksToRead, out SectorStatus[] statuses))
{
outputFormat.WriteSectorTag(new byte[5], i + j, false, SectorTagType.DvdTitleKeyDecrypted);
if(_stopOnError) return;
MarkTitleKeyDumped(i + j);
outputFormat.WriteSectorsLong(new byte[blockSize * _skip],
i,
false,
_skip,
Enumerable.Repeat(SectorStatus.NotDumped, (int)_skip).ToArray());
continue;
}
for(ulong b = i; b < i + _skip; b++) _resume.BadBlocks.Add(b);
CSS.DecryptTitleKey(discKey, key, out tmpBuf);
outputFormat.WriteSectorTag(tmpBuf, i + j, false, SectorTagType.DvdTitleKeyDecrypted);
MarkTitleKeyDumped(i + j);
if(_storeEncrypted) continue;
cmi[j] = buffer[2064 * j + 6];
}
// Todo: Flag in the outputFormat that a sector has been decrypted
if(!_storeEncrypted)
{
ErrorNumber errno =
outputFormat.ReadSectorsTag(i,
false,
blocksToRead,
SectorTagType.DvdTitleKeyDecrypted,
out byte[] titleKey);
if(errno != ErrorNumber.NoError)
{
ErrorMessage?.Invoke(string.Format(Localization.Core.Error_retrieving_title_key_for_sector_0,
i));
mhddLog.Write(i, cmdDuration < 500 ? 65535 : cmdDuration, _skip);
ibgLog.Write(i, 0);
AaruLogging.WriteLine(Localization.Core.Skipping_0_blocks_from_errored_block_1, _skip, i);
i += _skip - blocksToRead;
newTrim = true;
}
else
buffer = CSS.DecryptSectorLong(buffer, titleKey, cmi, blocksToRead);
{
outputFormat.WriteSectorsLong(buffer, i, false, blocksToRead, statuses);
}
}
else
{
var cmi = new byte[blocksToRead];
outputFormat.WriteSectorsLong(buffer,
i,
false,
blocksToRead,
Enumerable.Repeat(SectorStatus.Dumped, (int)blocksToRead).ToArray());
for (uint j = 0; j < blocksToRead; j++)
{
byte[] key = buffer.Skip((int)(2064 * j + 7)).Take(5).ToArray();
if(key.All(static k => k == 0))
{
outputFormat.WriteSectorTag(new byte[5], i + j, false, SectorTagType.DvdTitleKeyDecrypted);
MarkTitleKeyDumped(i + j);
continue;
}
CSS.DecryptTitleKey(discKey, key, out byte[] tmpBuf);
outputFormat.WriteSectorTag(tmpBuf, i + j, false, SectorTagType.DvdTitleKeyDecrypted);
MarkTitleKeyDumped(i + j);
if(_storeEncrypted) continue;
cmi[j] = buffer[2064 * j + 6];
}
// Todo: Flag in the outputFormat that a sector has been decrypted
if(!_storeEncrypted)
{
ErrorNumber errno =
outputFormat.ReadSectorsTag(i,
false,
blocksToRead,
SectorTagType.DvdTitleKeyDecrypted,
out byte[] titleKey);
if(errno != ErrorNumber.NoError)
{
ErrorMessage?.Invoke(string.Format(Localization.Core.Error_retrieving_title_key_for_sector_0,
i));
}
else
buffer = CSS.DecryptSectorLong(buffer, titleKey, cmi, blocksToRead);
}
outputFormat.WriteSectorsLong(buffer,
i,
false,
blocksToRead,
Enumerable.Repeat(SectorStatus.Dumped, (int)blocksToRead).ToArray());
}
imageWriteDuration += _writeStopwatch.Elapsed.TotalSeconds;
extents.Add(i, blocksToRead, true);

View File

@@ -101,45 +101,55 @@ partial class Dump
if(scsiReader.ReadBuffer3CReadRaw || scsiReader.OmniDriveReadRaw || scsiReader.HldtstReadRaw)
{
var cmi = new byte[1];
byte[] key = buffer.Skip(7).Take(5).ToArray();
if(key.All(static k => k == 0))
if(_ngcwEnabled)
{
outputFormat.WriteSectorTag(new byte[5], badSector, false, SectorTagType.DvdTitleKeyDecrypted);
MarkTitleKeyDumped(badSector);
if(TransformNgcwLongSectors(scsiReader, buffer, badSector, 1, out SectorStatus[] statuses))
outputFormat.WriteSectorLong(buffer, badSector, false, statuses[0]);
else
outputFormat.WriteSectorLong(buffer, badSector, false, SectorStatus.NotDumped);
}
else
{
CSS.DecryptTitleKey(discKey, key, out byte[] tmpBuf);
outputFormat.WriteSectorTag(tmpBuf, badSector, false, SectorTagType.DvdTitleKeyDecrypted);
MarkTitleKeyDumped(badSector);
var cmi = new byte[1];
cmi[0] = buffer[6];
}
byte[] key = buffer.Skip(7).Take(5).ToArray();
if(!_storeEncrypted)
{
ErrorNumber errno =
outputFormat.ReadSectorsTag(badSector,
false,
1,
SectorTagType.DvdTitleKeyDecrypted,
out byte[] titleKey);
if(errno != ErrorNumber.NoError)
if(key.All(static k => k == 0))
{
ErrorMessage?.Invoke(string.Format(Localization.Core.Error_retrieving_title_key_for_sector_0,
badSector));
outputFormat.WriteSectorTag(new byte[5], badSector, false, SectorTagType.DvdTitleKeyDecrypted);
MarkTitleKeyDumped(badSector);
}
else
buffer = CSS.DecryptSectorLong(buffer, titleKey, cmi);
}
{
CSS.DecryptTitleKey(discKey, key, out byte[] tmpBuf);
outputFormat.WriteSectorTag(tmpBuf, badSector, false, SectorTagType.DvdTitleKeyDecrypted);
MarkTitleKeyDumped(badSector);
_resume.BadBlocks.Remove(badSector);
outputFormat.WriteSectorLong(buffer, badSector, false, SectorStatus.Dumped);
cmi[0] = buffer[6];
}
if(!_storeEncrypted)
{
ErrorNumber errno =
outputFormat.ReadSectorsTag(badSector,
false,
1,
SectorTagType.DvdTitleKeyDecrypted,
out byte[] titleKey);
if(errno != ErrorNumber.NoError)
{
ErrorMessage?.Invoke(string.Format(Localization.Core.Error_retrieving_title_key_for_sector_0,
badSector));
}
else
buffer = CSS.DecryptSectorLong(buffer, titleKey, cmi);
}
_resume.BadBlocks.Remove(badSector);
outputFormat.WriteSectorLong(buffer, badSector, false, SectorStatus.Dumped);
}
}
else
outputFormat.WriteSector(buffer, badSector, false, SectorStatus.Dumped);

View File

@@ -77,6 +77,8 @@ sealed partial class Reader
internal uint PhysicalBlockSize { get; private set; }
internal uint LongBlockSize { get; private set; }
internal bool CanReadRaw { get; private set; }
/// <summary>When true with OmniDrive raw reads, use descramble=0 and software Nintendo descrambling (GameCube/Wii).</summary>
internal bool OmniDriveNintendoMode { get; set; }
internal bool CanSeek => _ataSeek || _seek6 || _seek10;
internal bool CanSeekLba => _ataSeekLba || _seek6 || _seek10;

View File

@@ -588,11 +588,12 @@ sealed partial class Reader
ReadBuffer3CReadRaw =
!_dev.ReadBuffer3CRawDvd(out _, out senseBuf, 0, 1, _timeout, out _, layerbreak, otp);
// Try OmniDrive on drives with OmniDrive firmware
// Try OmniDrive on drives with OmniDrive firmware (standard descramble=1 and Nintendo descramble=0)
if(_dev.IsOmniDriveFirmware())
{
OmniDriveReadRaw =
!_dev.OmniDriveReadRawDvd(out _, out senseBuf, 0, 1, _timeout, out _);
bool omniStandardOk = !_dev.OmniDriveReadRawDvd(out _, out senseBuf, 0, 1, _timeout, out _);
bool omniNintendoOk = !_dev.OmniDriveReadNintendoDvd(out _, out senseBuf, 0, 1, _timeout, out _);
OmniDriveReadRaw = omniStandardOk || omniNintendoOk;
}
if(HldtstReadRaw || _plextorReadRaw || ReadBuffer3CReadRaw || OmniDriveReadRaw)
@@ -848,12 +849,21 @@ sealed partial class Reader
else if(OmniDriveReadRaw)
{
uint lba = negative ? (uint)(-(long)block) : (uint)block;
sense = _dev.OmniDriveReadRawDvd(out buffer,
out senseBuf,
lba,
count,
_timeout,
out duration);
if(OmniDriveNintendoMode)
sense = _dev.OmniDriveReadNintendoDvd(out buffer,
out senseBuf,
lba,
count,
_timeout,
out duration);
else
sense = _dev.OmniDriveReadRawDvd(out buffer,
out senseBuf,
lba,
count,
_timeout,
out duration);
}
else if(ReadBuffer3CReadRaw)
{

View File

@@ -41,8 +41,11 @@ namespace Aaru.Decoders.Nintendo;
[SuppressMessage("ReSharper", "UnusedMember.Global")]
public sealed class Sector
{
/// <summary>Offset of main user data in a Nintendo (GameCube/Wii) DVD sector (bytes 6-2053).</summary>
public const int NintendoMainDataOffset = 6;
/// <summary>
/// ECMA-267 <c>main_data</c> offset in OmniDrive 2064-byte Nintendo sectors: DVD XOR applies to 2048 bytes from
/// here (same as standard DVD). Bytes 6-11 (<c>cpr_mai</c>) are not scrambled on media.
/// </summary>
public const int NintendoMainDataOffset = 12;
/// <summary>
/// Derives the Nintendo descramble key from the first 8 bytes of the cpr_mai region (LBA 0 payload).

View File

@@ -41,6 +41,45 @@ namespace Aaru.Devices;
public partial class Device
{
enum OmniDriveDiscType
{
CD = 0,
DVD = 1,
BD = 2
}
/// <summary>
/// Encodes byte 1 of the OmniDrive READ CDB to match redumper's <c>CDB12_ReadOmniDrive</c>
/// (<c>scsi/mmc.ixx</c>): <c>disc_type</c> :2 (LSB), <c>raw_addressing</c> :1, <c>fua</c> :1, <c>descramble</c> :1, reserved :3.
/// </summary>
/// <param name="discType">0 = CD, 1 = DVD, 2 = BD (redumper <c>OmniDrive_DiscType</c>).</param>
static byte EncodeOmniDriveReadCdb1(OmniDriveDiscType discType, bool rawAddressing, bool fua, bool descramble)
{
int d = (byte)discType & 3;
int r = rawAddressing ? 1 : 0;
int f = fua ? 1 : 0;
int s = descramble ? 1 : 0;
return (byte)(d | (r << 2) | (f << 3) | (s << 4));
}
static void FillOmniDriveReadDvdCdb(Span<byte> cdb, uint lba, uint transferLength, byte cdbByte1)
{
cdb.Clear();
cdb[0] = (byte)ScsiCommands.ReadOmniDrive;
cdb[1] = cdbByte1;
cdb[2] = (byte)((lba >> 24) & 0xFF);
cdb[3] = (byte)((lba >> 16) & 0xFF);
cdb[4] = (byte)((lba >> 8) & 0xFF);
cdb[5] = (byte)(lba & 0xFF);
cdb[6] = (byte)((transferLength >> 24) & 0xFF);
cdb[7] = (byte)((transferLength >> 16) & 0xFF);
cdb[8] = (byte)((transferLength >> 8) & 0xFF);
cdb[9] = (byte)(transferLength & 0xFF);
cdb[10] = 0; // subchannels=NONE, c2=0
cdb[11] = 0; // control
}
/// <summary>
/// Checks if the drive has OmniDrive firmware by inspecting INQUIRY Reserved5 (bytes 74+) for "OmniDrive",
/// matching redumper's is_omnidrive_firmware behaviour.
@@ -77,27 +116,19 @@ public partial class Device
/// <param name="transferLength">Number of 2064-byte sectors to read.</param>
/// <param name="timeout">Timeout in seconds.</param>
/// <param name="duration">Duration in milliseconds it took for the device to execute the command.</param>
/// <param name="fua">Set to <c>true</c> if the command should use FUA.</param>
/// <param name="descramble">Set to <c>true</c> if the data should be descrambled by the device.</param>
public bool OmniDriveReadRawDvd(out byte[] buffer, out ReadOnlySpan<byte> senseBuffer, uint lba, uint transferLength,
uint timeout, out double duration)
uint timeout, out double duration, bool fua = false, bool descramble = true)
{
senseBuffer = SenseBuffer;
Span<byte> cdb = CdbBuffer[..12];
cdb.Clear();
buffer = new byte[2064 * transferLength];
cdb[0] = (byte)ScsiCommands.ReadOmniDrive;
cdb[1] = 0x11; // disc_type=1 (DVD), raw_addressing=0 (LBA), fua=0, descramble=1
cdb[2] = (byte)((lba >> 24) & 0xFF);
cdb[3] = (byte)((lba >> 16) & 0xFF);
cdb[4] = (byte)((lba >> 8) & 0xFF);
cdb[5] = (byte)(lba & 0xFF);
cdb[6] = (byte)((transferLength >> 24) & 0xFF);
cdb[7] = (byte)((transferLength >> 16) & 0xFF);
cdb[8] = (byte)((transferLength >> 8) & 0xFF);
cdb[9] = (byte)(transferLength & 0xFF);
cdb[10] = 0; // subchannels=NONE, c2=0
cdb[11] = 0; // control
FillOmniDriveReadDvdCdb(cdb,
lba,
transferLength,
EncodeOmniDriveReadCdb1(OmniDriveDiscType.DVD, false, fua, descramble));
LastError = SendScsiCommand(cdb, ref buffer, timeout, ScsiDirection.In, out duration, out bool sense);
@@ -109,4 +140,32 @@ public partial class Device
return sense;
}
/// <summary>
/// Reads raw Nintendo GameCube/Wii DVD sectors (2064 bytes) on OmniDrive. Default matches redumper raw DVD
/// (<c>descramble</c> off); use software descramble via Aaru.Decoders.Nintendo.Sector when needed.
/// </summary>
/// <param name="descramble">Drive-side DVD descramble (redumper raw DVD uses <c>false</c>).</param>
/// <returns><c>true</c> if the command failed and <paramref name="senseBuffer" /> contains the sense buffer.</returns>
public bool OmniDriveReadNintendoDvd(out byte[] buffer, out ReadOnlySpan<byte> senseBuffer, uint lba, uint transferLength,
uint timeout, out double duration, bool descramble = false)
{
senseBuffer = SenseBuffer;
Span<byte> cdb = CdbBuffer[..12];
buffer = new byte[2064 * transferLength];
FillOmniDriveReadDvdCdb(cdb,
lba,
transferLength,
EncodeOmniDriveReadCdb1(OmniDriveDiscType.DVD, false, false, descramble));
LastError = SendScsiCommand(cdb, ref buffer, timeout, ScsiDirection.In, out duration, out bool sense);
// Scrambled Nintendo sectors do not pass standard DVD EDC until software-descrambled.
Error = LastError != 0;
AaruLogging.Debug(SCSI_MODULE_NAME, "OmniDrive READ NINTENDO DVD took {0} ms", duration);
return sense;
}
}

View File

@@ -925,6 +925,18 @@
<data name="Dumping_Nintendo_GameCube_or_Wii_discs_is_not_yet_implemented" xml:space="preserve">
<value>[red]El volcado de discos de Nintendo GameCube o Wii no está implementado todavía.[/]</value>
</data>
<data name="Nintendo_OmniDrive_dump_requires_Aaru_format" xml:space="preserve">
<value>[red]El volcado de discos de Nintendo GameCube o Wii con OmniDrive requiere formato de salida Aaru.[/]</value>
</data>
<data name="Ngcw_dump_parsing_partition_table" xml:space="preserve">
<value>[slateblue1]Analizando tabla de particiones de Wii...[/]</value>
</data>
<data name="Ngcw_dump_wrote_partition_key_map" xml:space="preserve">
<value>[slateblue1]Se guardó el mapa de claves de partición de Wii.[/]</value>
</data>
<data name="Ngcw_dump_wrote_junk_map_0_entries" xml:space="preserve">
<value>[slateblue1]Se guardó el mapa de basura de Nintendo con [lime]{0}[/] entradas.[/]</value>
</data>
<data name="Dumping_progress_log" xml:space="preserve">
<value>################# Registro de progreso del volcado #################</value>
</data>

View File

@@ -1395,6 +1395,18 @@
<data name="Dumping_Nintendo_GameCube_or_Wii_discs_is_not_yet_implemented" xml:space="preserve">
<value>[red]Dumping Nintendo GameCube or Wii discs is not yet implemented.[/]</value>
</data>
<data name="Nintendo_OmniDrive_dump_requires_Aaru_format" xml:space="preserve">
<value>[red]Dumping Nintendo GameCube or Wii discs with OmniDrive requires Aaru output format.[/]</value>
</data>
<data name="Ngcw_dump_parsing_partition_table" xml:space="preserve">
<value>[slateblue1]Parsing Wii partition table...[/]</value>
</data>
<data name="Ngcw_dump_wrote_partition_key_map" xml:space="preserve">
<value>[slateblue1]Stored Wii partition key map.[/]</value>
</data>
<data name="Ngcw_dump_wrote_junk_map_0_entries" xml:space="preserve">
<value>[slateblue1]Stored Nintendo junk map with [lime]{0}[/] entries.[/]</value>
</data>
<data name="Reading_Disc_Manufacturing_Information" xml:space="preserve">
<value>[slateblue1]Reading [italic]Disc Manufacturing Information[/][/]</value>
</data>

View File

@@ -1,4 +1,4 @@
//------------------------------------------------------------------------------
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
@@ -1983,6 +1983,18 @@ namespace Aaru.Localization {
}
}
public static string Ngcw_nintendo_software_descramble {
get {
return ResourceManager.GetString("Ngcw_nintendo_software_descramble", resourceCulture);
}
}
public static string Ngcw_nintendo_derived_key_0 {
get {
return ResourceManager.GetString("Ngcw_nintendo_derived_key_0", resourceCulture);
}
}
public static string PS3_disc_key_resolved_from_0 {
get {
return ResourceManager.GetString("PS3_disc_key_resolved_from_0", resourceCulture);

View File

@@ -663,6 +663,12 @@
</data>
<data name="Ngcw_disc_number_0" xml:space="preserve">
<value>[slateblue1]Número de disco: [green]{0}[/].[/]</value>
</data>
<data name="Ngcw_nintendo_software_descramble" xml:space="preserve">
<value>[slateblue1]Descifrado por software de Nintendo activado (OmniDrive en bruto, descramble=0).[/]</value>
</data>
<data name="Ngcw_nintendo_derived_key_0" xml:space="preserve">
<value>[slateblue1]Clave de disco Nintendo derivada: [green]{0}[/].[/]</value>
</data>
<data name="PS3_disc_key_resolved_from_0" xml:space="preserve">
<value>[slateblue1]Clave de disco PS3 resuelta desde [aqua]{0}[/].[/]</value>

View File

@@ -1002,6 +1002,12 @@ In you are unsure, please press N to not continue.</value>
</data>
<data name="Ngcw_disc_number_0" xml:space="preserve">
<value>[slateblue1]Disc number: [green]{0}[/].[/]</value>
</data>
<data name="Ngcw_nintendo_software_descramble" xml:space="preserve">
<value>[slateblue1]Nintendo software descrambling enabled (OmniDrive raw, descramble=0).[/]</value>
</data>
<data name="Ngcw_nintendo_derived_key_0" xml:space="preserve">
<value>[slateblue1]Derived Nintendo disc key: [green]{0}[/].[/]</value>
</data>
<data name="PS3_disc_key_resolved_from_0" xml:space="preserve">
<value>[slateblue1]PS3 disc key resolved from [aqua]{0}[/].[/]</value>