From e99ba48f0738f6da23e130732606cf82c4d0bbfb Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Sun, 15 Jan 2023 02:07:56 -0800 Subject: [PATCH] Determine PLJv2 Block 3 format, add notes --- BurnOutSharp.Builders/PlayJ.cs | 122 ++++++++++++++++----- BurnOutSharp.Models/PlayJ/AudioFile.cs | 23 +++- BurnOutSharp.Models/PlayJ/DataFile.cs | 35 ++++++ BurnOutSharp.Models/PlayJ/UnknownBlock1.cs | 8 +- BurnOutSharp.Models/PlayJ/UnknownBlock3.cs | 5 + BurnOutSharp.Wrappers/PlayJAudioFile.cs | 64 ++++++++++- 6 files changed, 220 insertions(+), 37 deletions(-) create mode 100644 BurnOutSharp.Models/PlayJ/DataFile.cs diff --git a/BurnOutSharp.Builders/PlayJ.cs b/BurnOutSharp.Builders/PlayJ.cs index 56632611..4ae28ab2 100644 --- a/BurnOutSharp.Builders/PlayJ.cs +++ b/BurnOutSharp.Builders/PlayJ.cs @@ -168,47 +168,88 @@ namespace BurnOutSharp.Builders #endregion - #region Unknown Value 2 + #region V1 Only - // If we have an unknown value 2 offset - if (entryHeader.UnknownOffset2 > 0) + // If we have a V1 file + if (entryHeader.Version == 0x00000000) { - // Get the unknown value 2 offset - long offset = entryHeader.UnknownOffset2 + adjust; - if (offset < 0 || offset >= data.Length) + #region Unknown Value 2 + + // If we have an unknown value 2 offset + if (entryHeader.UnknownOffset2 > 0) + { + // Get the unknown value 2 offset + long offset = entryHeader.UnknownOffset2 + adjust; + if (offset < 0 || offset >= data.Length) + return null; + + // Seek to the unknown value 2 + data.Seek(offset, SeekOrigin.Begin); + } + + // Set the unknown value 2 + audioFile.UnknownValue2 = data.ReadUInt32(); + + #endregion + + #region Unknown Block 3 + + // If we have an unknown block 3 offset + if (entryHeader.UnknownOffset2 > 0) + { + // Get the unknown block 3 offset + long offset = entryHeader.UnknownOffset3 + adjust; + if (offset < 0 || offset >= data.Length) + return null; + + // Seek to the unknown block 3 + data.Seek(offset, SeekOrigin.Begin); + } + + // Try to parse the unknown block 3 + var unknownBlock3 = ParseUnknownBlock3(data); + if (unknownBlock3 == null) return null; - // Seek to the unknown value 2 - data.Seek(offset, SeekOrigin.Begin); - } + // Set the unknown block 3 + audioFile.UnknownBlock3 = unknownBlock3; - // Set the unknown value 2 - audioFile.UnknownValue2 = data.ReadUInt32(); + #endregion + } #endregion - #region Unknown Block 3 + #region V2 Only - // If we have an unknown block 3 offset - if (entryHeader.UnknownOffset3 > 0) + // If we have a V2 file + if (entryHeader.Version == 0x0000000A) { - // Get the unknown block 3 offset - long offset = entryHeader.UnknownOffset3 + adjust; - if (offset < 0 || offset >= data.Length) - return null; + #region Data Files Count - // Seek to the unknown block 3 - data.Seek(offset, SeekOrigin.Begin); + // Set the data files count + audioFile.DataFilesCount = data.ReadUInt32(); + + #endregion + + #region Data Files + + // Create the data files array + audioFile.DataFiles = new DataFile[audioFile.DataFilesCount]; + + // Try to parse the data files + for (int i = 0; i < audioFile.DataFiles.Length; i++) + { + var dataFile = ParseDataFile(data); + if (dataFile == null) + return null; + + audioFile.DataFiles[i] = dataFile; + } + + + #endregion } - // Try to parse the unknown block 3 - var unknownBlock3 = ParseUnknownBlock3(data); - if (unknownBlock3 == null) - return null; - - // Set the unknown block 3 - audioFile.UnknownBlock3 = unknownBlock3; - #endregion return audioFile; @@ -326,8 +367,8 @@ namespace BurnOutSharp.Builders // TODO: Use marshalling here instead of building UnknownBlock1 unknownBlock1 = new UnknownBlock1(); - unknownBlock1.Length = data.ReadUInt16(); - unknownBlock1.Data = data.ReadBytes(unknownBlock1.Length); + unknownBlock1.Length = data.ReadUInt32(); + unknownBlock1.Data = data.ReadBytes((int)unknownBlock1.Length); return unknownBlock1; } @@ -347,6 +388,27 @@ namespace BurnOutSharp.Builders return unknownBlock3; } + /// + /// Parse a Stream into a data file + /// + /// Stream to parse + /// Filled data file on success, null on error + private static DataFile ParseDataFile(Stream data) + { + // TODO: Use marshalling here instead of building + DataFile dataFile = new DataFile(); + + dataFile.FileNameLength = data.ReadUInt16(); + byte[] fileName = data.ReadBytes(dataFile.FileNameLength); + if (fileName != null) + dataFile.FileName = Encoding.ASCII.GetString(fileName); + + dataFile.DataLength = data.ReadUInt32(); + dataFile.Data = data.ReadBytes((int)dataFile.DataLength); + + return dataFile; + } + #endregion } } diff --git a/BurnOutSharp.Models/PlayJ/AudioFile.cs b/BurnOutSharp.Models/PlayJ/AudioFile.cs index 04716a05..8d2578aa 100644 --- a/BurnOutSharp.Models/PlayJ/AudioFile.cs +++ b/BurnOutSharp.Models/PlayJ/AudioFile.cs @@ -15,6 +15,8 @@ namespace BurnOutSharp.Models.PlayJ /// public UnknownBlock1 UnknownBlock1 { get; set; } + #region V1 Only + /// /// Value referred to by /// @@ -22,8 +24,27 @@ namespace BurnOutSharp.Models.PlayJ public uint UnknownValue2 { get; set; } /// - /// Unknown block 3 + /// Unknown block 3 (V1 only) /// public UnknownBlock3 UnknownBlock3 { get; set; } + + #endregion + + #region V2 Only + + /// + /// Number of data files embedded + /// + public uint DataFilesCount { get; set; } + + /// + /// Data files (V2 only) + /// + public DataFile[] DataFiles { get; set; } + + // After the data files is a block starting with 0x00000001 + // This block then contains highly repeating data, possible audio samples? + + #endregion } } \ No newline at end of file diff --git a/BurnOutSharp.Models/PlayJ/DataFile.cs b/BurnOutSharp.Models/PlayJ/DataFile.cs new file mode 100644 index 00000000..310c25ea --- /dev/null +++ b/BurnOutSharp.Models/PlayJ/DataFile.cs @@ -0,0 +1,35 @@ +namespace BurnOutSharp.Models.PlayJ +{ + /// + /// Embedded data file (V2 only?) + /// + public sealed class DataFile + { + /// + /// Length of the data file name + /// + public ushort FileNameLength; + + /// + /// Data file name + /// + public string FileName; + + /// + /// Length of the data + /// + public uint DataLength; + + /// + /// Data + /// + public byte[] Data; + + // Notes about Data: + // - Each data block in the samples contains a GIF header + // - Each GIF header contains an application extension: http://www.vurdalakov.net/misc/gif/application-extension + // - Each GIF doesn't always stretch to the end of the data + // - Remaining data seems to be padded as 0x00 (typically 8 bytes) + // - GIF data is fully formed and may be copied to a standalone file + } +} \ No newline at end of file diff --git a/BurnOutSharp.Models/PlayJ/UnknownBlock1.cs b/BurnOutSharp.Models/PlayJ/UnknownBlock1.cs index c7b7a9ec..5dfc1eb6 100644 --- a/BurnOutSharp.Models/PlayJ/UnknownBlock1.cs +++ b/BurnOutSharp.Models/PlayJ/UnknownBlock1.cs @@ -8,11 +8,17 @@ namespace BurnOutSharp.Models.PlayJ /// /// Length of the following data block /// - public ushort Length; + public uint Length; /// /// Unknown data /// public byte[] Data; + + // Notes about Data: + // - Might be UInt16 offset and UInt16 length pairs + // - Might be relevant to ad data + // - Might be relevant to encryption + // - Highly repeating patterns in the values with only a few differences } } \ No newline at end of file diff --git a/BurnOutSharp.Models/PlayJ/UnknownBlock3.cs b/BurnOutSharp.Models/PlayJ/UnknownBlock3.cs index 1cf366ab..12f1d2a0 100644 --- a/BurnOutSharp.Models/PlayJ/UnknownBlock3.cs +++ b/BurnOutSharp.Models/PlayJ/UnknownBlock3.cs @@ -9,5 +9,10 @@ namespace BurnOutSharp.Models.PlayJ /// Unknown data /// public byte[] Data; + + // Notes about Data: + // - This may be where the encrypted audio samples live + // - It is also possible that it's where the ad data lives and samples follow + // + See V2 for example of why this would be the case } } \ No newline at end of file diff --git a/BurnOutSharp.Wrappers/PlayJAudioFile.cs b/BurnOutSharp.Wrappers/PlayJAudioFile.cs index 31d15a82..8babc0f0 100644 --- a/BurnOutSharp.Wrappers/PlayJAudioFile.cs +++ b/BurnOutSharp.Wrappers/PlayJAudioFile.cs @@ -93,13 +93,15 @@ namespace BurnOutSharp.Wrappers #region Unknown Block 1 /// - public ushort UB1_Length => _audioFile.UnknownBlock1.Length; + public uint UB1_Length => _audioFile.UnknownBlock1.Length; /// public byte[] UB1_Data => _audioFile.UnknownBlock1.Data; #endregion + #region V1 Only + #region Unknown Value 2 /// @@ -116,6 +118,26 @@ namespace BurnOutSharp.Wrappers #endregion + #region V2 Only + + #region Data Files Count + + /// + public uint DataFilesCount => _audioFile.DataFilesCount; + + #endregion + + #region Unknown Block 3 + + /// + public Models.PlayJ.DataFile[] DataFiles => _audioFile.DataFiles; + + #endregion + + #endregion + + #endregion + #region Instance Variables /// @@ -192,8 +214,16 @@ namespace BurnOutSharp.Wrappers PrintEntryHeader(builder); PrintUnknownBlock1(builder); - PrintUnknownValue2(builder); - PrintUnknownBlock3(builder); + + if (Version == 0x00000000) + { + PrintUnknownValue2(builder); + PrintUnknownBlock3(builder); + } + else if (Version == 0x0000000A) + { + PrintDataFiles(builder); + } return builder; } @@ -249,7 +279,7 @@ namespace BurnOutSharp.Wrappers } /// - /// Print unknown value 2 information + /// Print unknown value 2 information (V1 only) /// /// StringBuilder to append information to private void PrintUnknownValue2(StringBuilder builder) @@ -261,7 +291,7 @@ namespace BurnOutSharp.Wrappers } /// - /// Print unknown block 3 information + /// Print unknown block 3 information (V1 only) /// /// StringBuilder to append information to private void PrintUnknownBlock3(StringBuilder builder) @@ -272,6 +302,30 @@ namespace BurnOutSharp.Wrappers builder.AppendLine(); } + /// + /// Print data files information (V2 only) + /// + /// StringBuilder to append information to + private void PrintDataFiles(StringBuilder builder) + { + builder.AppendLine(" Data Files Information:"); + builder.AppendLine(" -------------------------"); + builder.AppendLine($" Data files count: {DataFilesCount} (0x{DataFilesCount:X})"); + if (DataFilesCount != 0 && DataFiles != null && DataFiles.Length != 0) + { + for (int i = 0; i < DataFiles.Length; i++) + { + var dataFile = DataFiles[i]; + builder.AppendLine($" Data File {i}:"); + builder.AppendLine($" File name length: {dataFile.FileNameLength} (0x{dataFile.FileNameLength:X})"); + builder.AppendLine($" File name: {dataFile.FileName ?? "[NULL]"}"); + builder.AppendLine($" Data length: {dataFile.DataLength} (0x{dataFile.DataLength:X})"); + builder.AppendLine($" Data: {BitConverter.ToString(dataFile.Data ?? new byte[0]).Replace('-', ' ')}"); + } + } + builder.AppendLine(); + } + #if NET6_0_OR_GREATER ///