diff --git a/README.md b/README.md index 5b900fe..4837a33 100644 --- a/README.md +++ b/README.md @@ -33,9 +33,33 @@ dotnet build ```bash Usage: dfs [options] Commands: - create [--crc] Creates a DFS file from the specified directory with optional CRC. + create [--crc] [--base-path ] Creates a DFS file from the specified directory with optional CRC and base path. extract Extracts files from the specified DFS file to the specified path. verify Verifies the integrity of the specified DFS file. list Lists the contents of the specified DFS file. help Displays this help text. -``` \ No newline at end of file +``` + +### Remastering Area 51 assets + +The original Area 51 DFS archives store an absolute Windows path (e.g. `C:\GAMEDATA\A51\RELEASE\PC\`) for every file. The game uses this path to locate assets inside the archive, so the packed archive must use the same path. + +To repack a modified archive while preserving the original path: + +1. Find the base path stored in the original archive: + ```bash + dfs list + ``` + The path shown before each filename is the base path (e.g. `C:\GAMEDATA\A51\RELEASE\PC\`). + +2. Extract the original archive: + ```bash + dfs extract + ``` + +3. Replace or modify the files in ``. + +4. Repack with the original base path: + ```bash + dfs create --base-path "C:\GAMEDATA\A51\RELEASE\PC\" + ``` \ No newline at end of file diff --git a/dfs/Program.cs b/dfs/Program.cs index 63a2be4..ca1147a 100644 --- a/dfs/Program.cs +++ b/dfs/Program.cs @@ -31,7 +31,7 @@ static void PrintHelp() { Console.WriteLine("Usage: dfs [options]"); Console.WriteLine("Commands:"); - Console.WriteLine(" create [--crc] Creates a DFS file from the specified directory with optional CRC."); + Console.WriteLine(" create [--crc] [--base-path ] Creates a DFS file from the specified directory with optional CRC and base path."); Console.WriteLine(" extract Extracts files from the specified DFS file to the specified path."); Console.WriteLine(" verify Verifies the integrity of the specified DFS file."); Console.WriteLine(" list Lists the contents of the specified DFS file."); @@ -60,8 +60,26 @@ static bool CreateCommand(string[] args) return false; } - bool enableCrc = args.Length > 3 && args[3] == "--crc"; - Console.WriteLine($"Creating DFS file {outputFileName} from {inputDir} with {(enableCrc ? "CRC" : "no CRC")}..."); + bool enableCrc = false; + string? basePath = null; + for (int i = 3; i < args.Length; i++) + { + if (args[i] == "--crc") + { + enableCrc = true; + } + else if (args[i] == "--base-path") + { + if (i + 1 >= args.Length) + { + Console.Error.WriteLine("--base-path requires a value"); + return false; + } + basePath = args[++i]; + } + } + + Console.WriteLine($"Creating DFS file {outputFileName} from {inputDir} with {(enableCrc ? "CRC" : "no CRC")}{(basePath != null ? $" and base path '{basePath}'" : "")}..."); if (File.Exists(outputFileName)) { File.Delete(outputFileName); @@ -82,7 +100,7 @@ static bool CreateCommand(string[] args) .ThenBy(f => f.FileName) .ThenBy(f => f.Extension) .Select(f => f.FullPath); - using var writer = new DfsWriter(outputFileName, sourceFiles, sectorAligned, enableCrc: enableCrc); + using var writer = new DfsWriter(outputFileName, sourceFiles, sectorAligned, enableCrc: enableCrc, basePath: basePath); writer.Write(); return true; } diff --git a/dfslib/DfsWriter.cs b/dfslib/DfsWriter.cs index d6c1087..a061cee 100644 --- a/dfslib/DfsWriter.cs +++ b/dfslib/DfsWriter.cs @@ -14,6 +14,7 @@ public sealed class DfsWriter : IDisposable private readonly IEnumerable _filePaths; private readonly HashSet _sectorAlignedExtensions; private CheckSummer _checkSummer; + private readonly string? _basePath; private readonly StringTable _stringTable; @@ -42,11 +43,13 @@ public sealed class DfsWriter : IDisposable /// The split size to use for the DFS archive. /// The chunk size to use for the DFS archive. /// Whether to enable CRC checksums for the DFS archive. - public DfsWriter(string outputPath, IEnumerable filePaths, IEnumerable sectorAlignedExtensions, uint sectorSize = DfsConstants.DefaultSectorSize, uint splitSize = DfsConstants.DefaultSplitSize, uint chunkSize = DfsConstants.DefaultChunkSize, bool enableCrc = false) + /// Optional base path to store for all files in the archive (e.g. "C:\GAMEDATA\A51\RELEASE\PC\"). When provided, overrides the path derived from the actual file location. + public DfsWriter(string outputPath, IEnumerable filePaths, IEnumerable sectorAlignedExtensions, uint sectorSize = DfsConstants.DefaultSectorSize, uint splitSize = DfsConstants.DefaultSplitSize, uint chunkSize = DfsConstants.DefaultChunkSize, bool enableCrc = false, string? basePath = null) { _outputPath = outputPath; _filePaths = filePaths; _sectorAlignedExtensions = new HashSet(sectorAlignedExtensions, StringComparer.OrdinalIgnoreCase); + _basePath = basePath != null ? NormalizeBasePath(basePath) : null; _sectorSize = sectorSize; _splitSize = splitSize; _enableCrc = enableCrc; @@ -100,34 +103,42 @@ public void Write() // Assert that currentFile.Path is not empty Debug.Assert(!string.IsNullOrEmpty(currentFile.Path)); - // Set drive to always "C:\" - string root = Path.GetPathRoot(currentFile.Path); - - - - // Get the directory without the root - string directory = Path.GetDirectoryName(currentFile.Path) ?? string.Empty; - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + string pathForEntry; + if (_basePath != null) { - // Remove the drive root (e.g., "C:\") from the directory - if (!string.IsNullOrEmpty(root) && directory.StartsWith(root, StringComparison.OrdinalIgnoreCase)) - { - directory = directory[root.Length..]; - } + // Use the user-specified base path for all files + pathForEntry = _basePath; } else { - // On Unix-based systems, remove the leading forward slash - if (directory.StartsWith(Path.DirectorySeparatorChar.ToString())) + // Set drive to always "C:\" + string root = Path.GetPathRoot(currentFile.Path); + + // Get the directory without the root + string directory = Path.GetDirectoryName(currentFile.Path) ?? string.Empty; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - directory = directory[1..]; + // Remove the drive root (e.g., "C:\") from the directory + if (!string.IsNullOrEmpty(root) && directory.StartsWith(root, StringComparison.OrdinalIgnoreCase)) + { + directory = directory[root.Length..]; + } + } + else + { + // On Unix-based systems, remove the leading forward slash + if (directory.StartsWith(Path.DirectorySeparatorChar.ToString())) + { + directory = directory[1..]; + } } - } - // Normalize directory separators from '/' to '\\' - directory = directory.Replace('/', '\\'); - root = root.Replace('/', '\\'); + // Normalize directory separators from '/' to '\\' + directory = directory.Replace('/', '\\'); + root = root.Replace('/', '\\'); + pathForEntry = root + directory; + } string fileName = Path.GetFileNameWithoutExtension(currentFile.Path); string extension = Path.GetExtension(currentFile.Path); @@ -139,7 +150,7 @@ public void Write() (string substring1, string substring2) = FindCommonSubstring(previousFileName, fileName, nextFileName); // Add path substrings to dictionary - uint pathIndex = GetStringOffset(root + directory); + uint pathIndex = GetStringOffset(pathForEntry); uint fileNamePart1Index = GetStringOffset(substring1); uint fileNamePart2Index = GetStringOffset(substring2); uint extensionIndex = GetStringOffset(extension); @@ -440,4 +451,17 @@ private static (string, string) FindCommonSubstring(string str0, string str1, st return (str1[..commonLen].ToUpperInvariant(), str1[commonLen..].ToUpperInvariant()); } + /// + /// Normalizes a base path by replacing forward slashes with backslashes and ensuring it ends with a backslash. + /// + private static string NormalizeBasePath(string basePath) + { + basePath = basePath.Replace('/', '\\'); + if (!basePath.EndsWith('\\')) + { + basePath += '\\'; + } + return basePath; + } + } \ No newline at end of file