using System.Collections.Generic; using System.IO; using SabreTools.Data.Models.GCF; namespace SabreTools.Wrappers { public partial class GCF : WrapperBase { #region Descriptive Properties /// public override string DescriptionString => "Half-Life Game Cache File (GCF)"; #endregion #region Extension Properties /// public uint BlockSize => Model.DataBlockHeader.BlockSize; /// /// Set of all data block offsets /// public long[]? DataBlockOffsets { get { // Use the cached value, if it exists if (field is not null) return field; // Otherwise, build the data block set field = new long[Model.DataBlockHeader.BlockCount]; for (int i = 0; i < Model.DataBlockHeader.BlockCount; i++) { long dataBlockOffset = Model.DataBlockHeader.FirstBlockOffset + (i * Model.DataBlockHeader.BlockSize); field[i] = dataBlockOffset; } // Return the set of data blocks return field; } } = null; /// /// Set of all files and their information /// public FileInfo[]? Files { get { // Use the cached value, if it exists if (field is not null) return field; // Otherwise, scan and build the files var files = new List(); for (int i = 0; i < Model.DirectoryEntries.Length; i++) { // Get the directory entry var directoryEntry = Model.DirectoryEntries[i]; var directoryMapEntry = Model.DirectoryMapEntries[i]; // If we have a directory, skip for now #if NET20 || NET35 if ((directoryEntry.DirectoryFlags & HL_GCF_FLAG.HL_GCF_FLAG_FILE) == 0) #else if (!directoryEntry.DirectoryFlags.HasFlag(HL_GCF_FLAG.HL_GCF_FLAG_FILE)) #endif continue; // Otherwise, start building the file info var fileInfo = new FileInfo() { Size = directoryEntry.ItemSize, #if NET20 || NET35 Encrypted = (directoryEntry.DirectoryFlags & HL_GCF_FLAG.HL_GCF_FLAG_ENCRYPTED) != 0, #else Encrypted = directoryEntry.DirectoryFlags.HasFlag(HL_GCF_FLAG.HL_GCF_FLAG_ENCRYPTED), #endif }; var pathParts = new List { Model.DirectoryNames[directoryEntry.NameOffset] ?? string.Empty }; var blockEntries = new List(); // Traverse the parent tree uint index = directoryEntry.ParentIndex; while (index != 0xFFFFFFFF) { var parentDirectoryEntry = Model.DirectoryEntries[index]; pathParts.Add(Model.DirectoryNames[parentDirectoryEntry.NameOffset] ?? string.Empty); index = parentDirectoryEntry.ParentIndex; } // Traverse the block entries index = directoryMapEntry.FirstBlockIndex; while (index != Model.DataBlockHeader.BlockCount) { var nextBlock = Model.BlockEntries[index]; blockEntries.Add(nextBlock); index = nextBlock.NextBlockEntryIndex; } // Reverse the path parts because of traversal pathParts.Reverse(); // Build the remaining file info #if NET20 || NET35 string[] pathArray = [.. pathParts]; string tempPath = string.Empty; if (pathArray.Length == 0 || pathArray.Length == 1) { tempPath = pathArray[0]; } else { for (int j = 0; j < pathArray.Length; j++) { if (j == 0) tempPath = pathArray[j]; else tempPath = Path.Combine(tempPath, pathArray[j]); } } fileInfo.Path = tempPath; #else fileInfo.Path = Path.Combine([.. pathParts]); #endif fileInfo.BlockEntries = [.. blockEntries]; // Add the file info and continue files.Add(fileInfo); } // Set and return the file infos field = [.. files]; return field; } } = null; #endregion #region Constructors /// public GCF(Data.Models.GCF.File model, byte[] data) : base(model, data) { } /// public GCF(Data.Models.GCF.File model, byte[] data, int offset) : base(model, data, offset) { } /// public GCF(Data.Models.GCF.File model, byte[] data, int offset, int length) : base(model, data, offset, length) { } /// public GCF(Data.Models.GCF.File model, Stream data) : base(model, data) { } /// public GCF(Data.Models.GCF.File model, Stream data, long offset) : base(model, data, offset) { } /// public GCF(Data.Models.GCF.File model, Stream data, long offset, long length) : base(model, data, offset, length) { } #endregion #region Static Constructors /// /// Create an GCF from a byte array and offset /// /// Byte array representing the GCF /// Offset within the array to parse /// An GCF wrapper on success, null on failure public static GCF? Create(byte[]? data, int offset) { // If the data is invalid if (data is null || data.Length == 0) return null; // If the offset is out of bounds if (offset < 0 || offset >= data.Length) return null; // Create a memory stream and use that var dataStream = new MemoryStream(data, offset, data.Length - offset); return Create(dataStream); } /// /// Create a GCF from a Stream /// /// Stream representing the GCF /// An GCF wrapper on success, null on failure public static GCF? Create(Stream? data) { // If the data is invalid if (data is null || !data.CanRead) return null; try { // Cache the current offset long currentOffset = data.Position; var model = new Serialization.Readers.GCF().Deserialize(data); if (model is null) return null; return new GCF(model, data, currentOffset); } catch { return null; } } #endregion #region Helper Classes /// /// Class to contain all necessary file information /// public sealed class FileInfo { /// /// Full item path /// public string? Path; /// /// File size /// public uint Size; /// /// Indicates if the block is encrypted /// public bool Encrypted; /// /// Array of block entries /// public BlockEntry[] BlockEntries = []; } #endregion } }