From dd99ea5a09d4a52d110900043c20f182324e32d3 Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Fri, 8 Sep 2023 22:31:10 -0400 Subject: [PATCH] Add and convert NCF --- Bytes/NCF.Deserializer.cs | 27 ++ Streams/NCF.Deserialization.cs | 515 +++++++++++++++++++++++++++++++++ Streams/NCF.Serialization.cs | 15 + 3 files changed, 557 insertions(+) create mode 100644 Bytes/NCF.Deserializer.cs create mode 100644 Streams/NCF.Deserialization.cs create mode 100644 Streams/NCF.Serialization.cs diff --git a/Bytes/NCF.Deserializer.cs b/Bytes/NCF.Deserializer.cs new file mode 100644 index 00000000..232ec8b4 --- /dev/null +++ b/Bytes/NCF.Deserializer.cs @@ -0,0 +1,27 @@ +using System.IO; + +namespace SabreTools.Serialization.Bytes +{ + public partial class NCF : IByteSerializer + { + /// +#if NET48 + public Models.NCF.File Deserialize(byte[] data, int offset) +#else + public Models.NCF.File? Deserialize(byte[]? data, int offset) +#endif + { + // If the data is invalid + if (data == null) + return null; + + // If the offset is out of bounds + if (offset < 0 || offset >= data.Length) + return null; + + // Create a memory stream and parse that + MemoryStream dataStream = new MemoryStream(data, offset, data.Length - offset); + return new Streams.NCF().Deserialize(dataStream); + } + } +} \ No newline at end of file diff --git a/Streams/NCF.Deserialization.cs b/Streams/NCF.Deserialization.cs new file mode 100644 index 00000000..f6862950 --- /dev/null +++ b/Streams/NCF.Deserialization.cs @@ -0,0 +1,515 @@ +using System.Collections.Generic; +using System.IO; +using System.Text; +using SabreTools.IO; +using SabreTools.Models.NCF; + +namespace SabreTools.Serialization.Streams +{ + public partial class NCF : IStreamSerializer + { + /// +#if NET48 + public Models.NCF.File Deserialize(Stream data) +#else + public Models.NCF.File? Deserialize(Stream? data) +#endif + { + // If the data is invalid + if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) + return null; + + // If the offset is out of bounds + if (data.Position < 0 || data.Position >= data.Length) + return null; + + // Cache the current offset + long initialOffset = data.Position; + + // Create a new Half-Life No Cache to fill + var file = new Models.NCF.File(); + + #region Header + + // Try to parse the header + var header = ParseHeader(data); + if (header == null) + return null; + + // Set the no cache header + file.Header = header; + + #endregion + + // Cache the current offset + initialOffset = data.Position; + + #region Directory Header + + // Try to parse the directory header + var directoryHeader = ParseDirectoryHeader(data); + if (directoryHeader == null) + return null; + + // Set the game cache directory header + file.DirectoryHeader = directoryHeader; + + #endregion + + #region Directory Entries + + // Create the directory entry array + file.DirectoryEntries = new DirectoryEntry[directoryHeader.ItemCount]; + + // Try to parse the directory entries + for (int i = 0; i < directoryHeader.ItemCount; i++) + { + var directoryEntry = ParseDirectoryEntry(data); + file.DirectoryEntries[i] = directoryEntry; + } + + #endregion + + #region Directory Names + + if (directoryHeader.NameSize > 0) + { + // Get the current offset for adjustment + long directoryNamesStart = data.Position; + + // Get the ending offset + long directoryNamesEnd = data.Position + directoryHeader.NameSize; + + // Create the string dictionary + file.DirectoryNames = new Dictionary(); + + // Loop and read the null-terminated strings + while (data.Position < directoryNamesEnd) + { + long nameOffset = data.Position - directoryNamesStart; + string directoryName = data.ReadString(Encoding.ASCII); + if (data.Position > directoryNamesEnd) + { + data.Seek(-directoryName.Length, SeekOrigin.Current); + byte[] endingData = data.ReadBytes((int)(directoryNamesEnd - data.Position)); + if (endingData != null) + directoryName = Encoding.ASCII.GetString(endingData); + else + directoryName = null; + } + + file.DirectoryNames[nameOffset] = directoryName; + } + + // Loop and assign to entries + foreach (var directoryEntry in file.DirectoryEntries) + { + directoryEntry.Name = file.DirectoryNames[directoryEntry.NameOffset]; + } + } + + #endregion + + #region Directory Info 1 Entries + + // Create the directory info 1 entry array + file.DirectoryInfo1Entries = new DirectoryInfo1Entry[directoryHeader.Info1Count]; + + // Try to parse the directory info 1 entries + for (int i = 0; i < directoryHeader.Info1Count; i++) + { + var directoryInfo1Entry = ParseDirectoryInfo1Entry(data); + file.DirectoryInfo1Entries[i] = directoryInfo1Entry; + } + + #endregion + + #region Directory Info 2 Entries + + // Create the directory info 2 entry array + file.DirectoryInfo2Entries = new DirectoryInfo2Entry[directoryHeader.ItemCount]; + + // Try to parse the directory info 2 entries + for (int i = 0; i < directoryHeader.ItemCount; i++) + { + var directoryInfo2Entry = ParseDirectoryInfo2Entry(data); + file.DirectoryInfo2Entries[i] = directoryInfo2Entry; + } + + #endregion + + #region Directory Copy Entries + + // Create the directory copy entry array + file.DirectoryCopyEntries = new DirectoryCopyEntry[directoryHeader.CopyCount]; + + // Try to parse the directory copy entries + for (int i = 0; i < directoryHeader.CopyCount; i++) + { + var directoryCopyEntry = ParseDirectoryCopyEntry(data); + file.DirectoryCopyEntries[i] = directoryCopyEntry; + } + + #endregion + + #region Directory Local Entries + + // Create the directory local entry array + file.DirectoryLocalEntries = new DirectoryLocalEntry[directoryHeader.LocalCount]; + + // Try to parse the directory local entries + for (int i = 0; i < directoryHeader.LocalCount; i++) + { + var directoryLocalEntry = ParseDirectoryLocalEntry(data); + file.DirectoryLocalEntries[i] = directoryLocalEntry; + } + + #endregion + + // Seek to end of directory section, just in case + data.Seek(initialOffset + directoryHeader.DirectorySize, SeekOrigin.Begin); + + #region Unknown Header + + // Try to parse the unknown header + var unknownHeader = ParseUnknownHeader(data); + if (unknownHeader == null) + return null; + + // Set the game cache unknown header + file.UnknownHeader = unknownHeader; + + #endregion + + #region Unknown Entries + + // Create the unknown entry array + file.UnknownEntries = new UnknownEntry[directoryHeader.ItemCount]; + + // Try to parse the unknown entries + for (int i = 0; i < directoryHeader.ItemCount; i++) + { + var unknownEntry = ParseUnknownEntry(data); + file.UnknownEntries[i] = unknownEntry; + } + + #endregion + + #region Checksum Header + + // Try to parse the checksum header + var checksumHeader = ParseChecksumHeader(data); + if (checksumHeader == null) + return null; + + // Set the game cache checksum header + file.ChecksumHeader = checksumHeader; + + #endregion + + // Cache the current offset + initialOffset = data.Position; + + #region Checksum Map Header + + // Try to parse the checksum map header + var checksumMapHeader = ParseChecksumMapHeader(data); + if (checksumMapHeader == null) + return null; + + // Set the game cache checksum map header + file.ChecksumMapHeader = checksumMapHeader; + + #endregion + + #region Checksum Map Entries + + // Create the checksum map entry array + file.ChecksumMapEntries = new ChecksumMapEntry[checksumMapHeader.ItemCount]; + + // Try to parse the checksum map entries + for (int i = 0; i < checksumMapHeader.ItemCount; i++) + { + var checksumMapEntry = ParseChecksumMapEntry(data); + file.ChecksumMapEntries[i] = checksumMapEntry; + } + + #endregion + + #region Checksum Entries + + // Create the checksum entry array + file.ChecksumEntries = new ChecksumEntry[checksumMapHeader.ChecksumCount]; + + // Try to parse the checksum entries + for (int i = 0; i < checksumMapHeader.ChecksumCount; i++) + { + var checksumEntry = ParseChecksumEntry(data); + file.ChecksumEntries[i] = checksumEntry; + } + + #endregion + + // Seek to end of checksum section, just in case + data.Seek(initialOffset + checksumHeader.ChecksumSize, SeekOrigin.Begin); + + return file; + } + + /// + /// Parse a Stream into a Half-Life No Cache header + /// + /// Stream to parse + /// Filled Half-Life No Cache header on success, null on error + private static Header ParseHeader(Stream data) + { + // TODO: Use marshalling here instead of building + Header header = new Header(); + + header.Dummy0 = data.ReadUInt32(); + if (header.Dummy0 != 0x00000001) + return null; + + header.MajorVersion = data.ReadUInt32(); + if (header.MajorVersion != 0x00000002) + return null; + + header.MinorVersion = data.ReadUInt32(); + if (header.MinorVersion != 1) + return null; + + header.CacheID = data.ReadUInt32(); + header.LastVersionPlayed = data.ReadUInt32(); + header.Dummy1 = data.ReadUInt32(); + header.Dummy2 = data.ReadUInt32(); + header.FileSize = data.ReadUInt32(); + header.BlockSize = data.ReadUInt32(); + header.BlockCount = data.ReadUInt32(); + header.Dummy3 = data.ReadUInt32(); + + return header; + } + + /// + /// Parse a Stream into a Half-Life No Cache directory header + /// + /// Stream to parse + /// Filled Half-Life No Cache directory header on success, null on error + private static DirectoryHeader ParseDirectoryHeader(Stream data) + { + // TODO: Use marshalling here instead of building + DirectoryHeader directoryHeader = new DirectoryHeader(); + + directoryHeader.Dummy0 = data.ReadUInt32(); + if (directoryHeader.Dummy0 != 0x00000004) + return null; + + directoryHeader.CacheID = data.ReadUInt32(); + directoryHeader.LastVersionPlayed = data.ReadUInt32(); + directoryHeader.ItemCount = data.ReadUInt32(); + directoryHeader.FileCount = data.ReadUInt32(); + directoryHeader.ChecksumDataLength = data.ReadUInt32(); + directoryHeader.DirectorySize = data.ReadUInt32(); + directoryHeader.NameSize = data.ReadUInt32(); + directoryHeader.Info1Count = data.ReadUInt32(); + directoryHeader.CopyCount = data.ReadUInt32(); + directoryHeader.LocalCount = data.ReadUInt32(); + directoryHeader.Dummy1 = data.ReadUInt32(); + directoryHeader.Dummy2 = data.ReadUInt32(); + directoryHeader.Checksum = data.ReadUInt32(); + + return directoryHeader; + } + + /// + /// Parse a Stream into a Half-Life No Cache directory entry + /// + /// Stream to parse + /// Filled Half-Life No Cache directory entry on success, null on error + private static DirectoryEntry ParseDirectoryEntry(Stream data) + { + // TODO: Use marshalling here instead of building + DirectoryEntry directoryEntry = new DirectoryEntry(); + + directoryEntry.NameOffset = data.ReadUInt32(); + directoryEntry.ItemSize = data.ReadUInt32(); + directoryEntry.ChecksumIndex = data.ReadUInt32(); + directoryEntry.DirectoryFlags = (HL_NCF_FLAG)data.ReadUInt32(); + directoryEntry.ParentIndex = data.ReadUInt32(); + directoryEntry.NextIndex = data.ReadUInt32(); + directoryEntry.FirstIndex = data.ReadUInt32(); + + return directoryEntry; + } + + /// + /// Parse a Stream into a Half-Life No Cache directory info 1 entry + /// + /// Stream to parse + /// Filled Half-Life No Cache directory info 1 entry on success, null on error + private static DirectoryInfo1Entry ParseDirectoryInfo1Entry(Stream data) + { + // TODO: Use marshalling here instead of building + DirectoryInfo1Entry directoryInfo1Entry = new DirectoryInfo1Entry(); + + directoryInfo1Entry.Dummy0 = data.ReadUInt32(); + + return directoryInfo1Entry; + } + + /// + /// Parse a Stream into a Half-Life No Cache directory info 2 entry + /// + /// Stream to parse + /// Filled Half-Life No Cache directory info 2 entry on success, null on error + private static DirectoryInfo2Entry ParseDirectoryInfo2Entry(Stream data) + { + // TODO: Use marshalling here instead of building + DirectoryInfo2Entry directoryInfo2Entry = new DirectoryInfo2Entry(); + + directoryInfo2Entry.Dummy0 = data.ReadUInt32(); + + return directoryInfo2Entry; + } + + /// + /// Parse a Stream into a Half-Life No Cache directory copy entry + /// + /// Stream to parse + /// Filled Half-Life No Cache directory copy entry on success, null on error + private static DirectoryCopyEntry ParseDirectoryCopyEntry(Stream data) + { + // TODO: Use marshalling here instead of building + DirectoryCopyEntry directoryCopyEntry = new DirectoryCopyEntry(); + + directoryCopyEntry.DirectoryIndex = data.ReadUInt32(); + + return directoryCopyEntry; + } + + /// + /// Parse a Stream into a Half-Life No Cache directory local entry + /// + /// Stream to parse + /// Filled Half-Life No Cache directory local entry on success, null on error + private static DirectoryLocalEntry ParseDirectoryLocalEntry(Stream data) + { + // TODO: Use marshalling here instead of building + DirectoryLocalEntry directoryLocalEntry = new DirectoryLocalEntry(); + + directoryLocalEntry.DirectoryIndex = data.ReadUInt32(); + + return directoryLocalEntry; + } + + /// + /// Parse a Stream into a Half-Life No Cache unknown header + /// + /// Stream to parse + /// Filled Half-Life No Cache unknown header on success, null on error + private static UnknownHeader ParseUnknownHeader(Stream data) + { + // TODO: Use marshalling here instead of building + UnknownHeader unknownHeader = new UnknownHeader(); + + unknownHeader.Dummy0 = data.ReadUInt32(); + if (unknownHeader.Dummy0 != 0x00000001) + return null; + + unknownHeader.Dummy1 = data.ReadUInt32(); + if (unknownHeader.Dummy1 != 0x00000000) + return null; + + return unknownHeader; + } + + /// + /// Parse a Stream into a Half-Life No Cache unknown entry + /// + /// Stream to parse + /// Filled Half-Life No Cacheunknown entry on success, null on error + private static UnknownEntry ParseUnknownEntry(Stream data) + { + // TODO: Use marshalling here instead of building + UnknownEntry unknownEntry = new UnknownEntry(); + + unknownEntry.Dummy0 = data.ReadUInt32(); + + return unknownEntry; + } + + /// + /// Parse a Stream into a Half-Life No Cache checksum header + /// + /// Stream to parse + /// Filled Half-Life No Cache checksum header on success, null on error + private static ChecksumHeader ParseChecksumHeader(Stream data) + { + // TODO: Use marshalling here instead of building + ChecksumHeader checksumHeader = new ChecksumHeader(); + + checksumHeader.Dummy0 = data.ReadUInt32(); + if (checksumHeader.Dummy0 != 0x00000001) + return null; + + checksumHeader.ChecksumSize = data.ReadUInt32(); + + return checksumHeader; + } + + /// + /// Parse a Stream into a Half-Life No Cache checksum map header + /// + /// Stream to parse + /// Filled Half-Life No Cache checksum map header on success, null on error + private static ChecksumMapHeader ParseChecksumMapHeader(Stream data) + { + // TODO: Use marshalling here instead of building + ChecksumMapHeader checksumMapHeader = new ChecksumMapHeader(); + + checksumMapHeader.Dummy0 = data.ReadUInt32(); + if (checksumMapHeader.Dummy0 != 0x14893721) + return null; + + checksumMapHeader.Dummy1 = data.ReadUInt32(); + if (checksumMapHeader.Dummy1 != 0x00000001) + return null; + + checksumMapHeader.ItemCount = data.ReadUInt32(); + checksumMapHeader.ChecksumCount = data.ReadUInt32(); + + return checksumMapHeader; + } + + /// + /// Parse a Stream into a Half-Life No Cache checksum map entry + /// + /// Stream to parse + /// Filled Half-Life No Cache checksum map entry on success, null on error + private static ChecksumMapEntry ParseChecksumMapEntry(Stream data) + { + // TODO: Use marshalling here instead of building + ChecksumMapEntry checksumMapEntry = new ChecksumMapEntry(); + + checksumMapEntry.ChecksumCount = data.ReadUInt32(); + checksumMapEntry.FirstChecksumIndex = data.ReadUInt32(); + + return checksumMapEntry; + } + + /// + /// Parse a Stream into a Half-Life No Cache checksum entry + /// + /// Stream to parse + /// Filled Half-Life No Cache checksum entry on success, null on error + private static ChecksumEntry ParseChecksumEntry(Stream data) + { + // TODO: Use marshalling here instead of building + ChecksumEntry checksumEntry = new ChecksumEntry(); + + checksumEntry.Checksum = data.ReadUInt32(); + + return checksumEntry; + } + } +} \ No newline at end of file diff --git a/Streams/NCF.Serialization.cs b/Streams/NCF.Serialization.cs new file mode 100644 index 00000000..28c23f0c --- /dev/null +++ b/Streams/NCF.Serialization.cs @@ -0,0 +1,15 @@ +using System; +using System.IO; + +namespace SabreTools.Serialization.Streams +{ + public partial class NCF : IStreamSerializer + { + /// +#if NET48 + public Stream Serialize(Models.NCF.File obj) => throw new NotImplementedException(); +#else + public Stream? Serialize(Models.NCF.File? obj) => throw new NotImplementedException(); +#endif + } +} \ No newline at end of file