Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,33 @@ dotnet build
```bash
Usage: dfs <command> [options]
Commands:
create <inputDir> <outputFileName> [--crc] Creates a DFS file from the specified directory with optional CRC.
create <inputDir> <outputFileName> [--crc] [--base-path <path>] Creates a DFS file from the specified directory with optional CRC and base path.
extract <inputFile> <extractPath> Extracts files from the specified DFS file to the specified path.
verify <inputFile> Verifies the integrity of the specified DFS file.
list <inputFile> Lists the contents of the specified DFS file.
help Displays this help text.
```
```

### 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 <original.dfs>
```
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 <original.dfs> <outputDir>
```

3. Replace or modify the files in `<outputDir>`.

4. Repack with the original base path:
```bash
dfs create <outputDir> <repacked.dfs> --base-path "C:\GAMEDATA\A51\RELEASE\PC\"
```
26 changes: 22 additions & 4 deletions dfs/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ static void PrintHelp()
{
Console.WriteLine("Usage: dfs <command> [options]");
Console.WriteLine("Commands:");
Console.WriteLine(" create <inputDir> <outputFileName> [--crc] Creates a DFS file from the specified directory with optional CRC.");
Console.WriteLine(" create <inputDir> <outputFileName> [--crc] [--base-path <path>] Creates a DFS file from the specified directory with optional CRC and base path.");
Console.WriteLine(" extract <inputFile> <extractPath> Extracts files from the specified DFS file to the specified path.");
Console.WriteLine(" verify <inputFile> Verifies the integrity of the specified DFS file.");
Console.WriteLine(" list <inputFile> Lists the contents of the specified DFS file.");
Expand Down Expand Up @@ -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);
Expand All @@ -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;
}
Expand Down
70 changes: 47 additions & 23 deletions dfslib/DfsWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public sealed class DfsWriter : IDisposable
private readonly IEnumerable<string> _filePaths;
private readonly HashSet<string> _sectorAlignedExtensions;
private CheckSummer _checkSummer;
private readonly string? _basePath;

private readonly StringTable _stringTable;

Expand Down Expand Up @@ -42,11 +43,13 @@ public sealed class DfsWriter : IDisposable
/// <param name="splitSize">The split size to use for the DFS archive.</param>
/// <param name="chunkSize">The chunk size to use for the DFS archive.</param>
/// <param name="enableCrc">Whether to enable CRC checksums for the DFS archive.</param>
public DfsWriter(string outputPath, IEnumerable<string> filePaths, IEnumerable<string> sectorAlignedExtensions, uint sectorSize = DfsConstants.DefaultSectorSize, uint splitSize = DfsConstants.DefaultSplitSize, uint chunkSize = DfsConstants.DefaultChunkSize, bool enableCrc = false)
/// <param name="basePath">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.</param>
public DfsWriter(string outputPath, IEnumerable<string> filePaths, IEnumerable<string> 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<string>(sectorAlignedExtensions, StringComparer.OrdinalIgnoreCase);
_basePath = basePath != null ? NormalizeBasePath(basePath) : null;
_sectorSize = sectorSize;
_splitSize = splitSize;
_enableCrc = enableCrc;
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -440,4 +451,17 @@ private static (string, string) FindCommonSubstring(string str0, string str1, st
return (str1[..commonLen].ToUpperInvariant(), str1[commonLen..].ToUpperInvariant());
}

/// <summary>
/// Normalizes a base path by replacing forward slashes with backslashes and ensuring it ends with a backslash.
/// </summary>
private static string NormalizeBasePath(string basePath)
{
basePath = basePath.Replace('/', '\\');
if (!basePath.EndsWith('\\'))
{
basePath += '\\';
}
return basePath;
}

}