Conversation
CoD4FastFileHandler and CoD5FastFileHandler now accept an isXbox360 parameter to select the correct (signed) header for Xbox 360 files. FastFileHandlerFactory and FastFileProcessor.Compress are updated to detect the Xbox 360 platform and use the appropriate header bytes, ensuring correct FastFile creation for Xbox 360.
…gned/unsigned Simplify header logic by removing isXbox360 from CoD4/5 handlers and factory. Handlers now determine signed/unsigned header from the FastFile's IsSigned property during recompression, preserving the original file's status. Updated FastFileProcessor.Compress to use a signed parameter instead of platform-based detection. This improves accuracy and consistency when reading and writing FastFiles.
Update CoD4/CoD5 FastFile handlers to write a 256-byte signature block and compress zone data as a single zlib stream for Xbox 360 signed files. Preserve block-based format for unsigned files.
Rewrite signed FastFile output to use IWffs100 streaming header, preserve hash table/auth data, and compress zone with full zlib (best compression, 78 DA header). Write version in big-endian. Fallback to zeros if hash table is unavailable. Unsigned/PS3 block format is unchanged.
- Enable creation of Xbox 360 signed FastFiles (.ff) for CoD4 and WaW, preserving hash table from original signed files - Add UI options for Xbox 360 signed formats and prompt user for original FF when required - Refactor CoD4/CoD5 handlers to use new FastFileProcessor.CompressXbox360Signed method - Implement streaming header, hash table, and zlib stream logic for signed format in FastFileProcessor and FastFileConstants - Extend Compiler to support direct compilation to Xbox 360 signed format - Ensure correct headers and version bytes for all formats - Maintain compatibility with unsigned and other platform FastFiles
Replaced separate COD5, COD4, and MW2 FastFile menu items with a single "Open FastFile..." option that auto-detects the game type. Updated the menu item's tooltip and shortcut, and removed all related submenu code and event handlers. The file dialog now allows selection of any FastFile type, streamlining the user experience.
Adds a "Platform" dropdown to the GUI, allowing users to select the target platform (PS3, Xbox 360 unsigned, Xbox 360 signed, PC, Wii) for FastFile compilation. The compile logic and Compiler class are updated to use the selected platform, ensuring correct versioning and format for each output type. Xbox 360 signed output now requires loading an original signed FastFile to extract the hash table, with appropriate user warnings. The About dialog and UI layout are updated to reflect multi-platform support. MW2FastFileHandler and related logic now preserve and handle signed/unsigned status correctly during recompression.
Refactored the Recompress method to always output unsigned block format for MW2 FastFiles, regardless of original signature status. Removed conditional logic for signed Xbox 360 files and legacy streaming format. Now always writes IWffu100 header, preserves version, adds minimal extended header, compresses in 64KB blocks, and appends explicit end marker. Updated comments to clarify MW2 XBlock vs. older formats and rationale for unsigned output.
Move MW2 FastFile compression logic from MW2FastFileHandler to a new FastFileProcessor.CompressMW2 method. This centralizes and unifies platform-specific compression for PS3 (block-based) and Xbox 360/PC (single zlib stream). Removes old inlined helpers and adds reusable methods for block compression and extended header writing, improving maintainability and correctness.
There was a problem hiding this comment.
Pull request overview
This PR introduces version 3.1.0 of the FastFile Tool suite, adding comprehensive support for Xbox 360 signed format FastFiles across all GUI applications. The update expands platform support beyond PS3 to include Xbox 360 (both signed and unsigned variants), PC, and Wii platforms.
Key Changes:
- Added Xbox 360 signed format support with hash table preservation from original files
- Expanded platform-specific compression methods including MW2 format support
- Enhanced all three GUI applications with platform selection and version display
Reviewed changes
Copilot reviewed 20 out of 23 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| FastFileToolGUI/MainForm.cs | Added Xbox 360 signed format handling with original FF validation and platform selection expansion |
| FastFileToolGUI/MainForm.Designer.cs | Updated window title to include version number |
| FastFileToolGUI/FastFileToolGUI.csproj | Bumped version to 3.1.0 |
| FastFileLib/FastFileProcessor.cs | Implemented Xbox 360 signed compression, MW2 compression methods, and full zlib compression |
| FastFileLib/FastFileLib.csproj | Bumped version to 3.1.0 |
| FastFileLib/FastFileConverter.cs | Updated zone header patching to support platform-specific offsets and CoD4/WaW differences |
| FastFileLib/FastFileConstants.cs | Added Xbox 360 signed format constants and platform-specific zone header offset helpers |
| FastFileLib/Compiler.cs | Refactored constructor to support platform selection and added Xbox 360 signed compilation |
| FastFileConverterGUI/Form1.cs | Updated window title to include version number |
| FastFileConverterGUI/FastFileConverterGUI.csproj | Bumped version to 3.1.0 |
| FastFileCompilerGUI/MainForm.cs | Added platform dropdown, Xbox 360 signed validation, and updated compilation workflow |
| FastFileCompilerGUI/MainForm.Designer.cs | Added platform selection UI controls and tooltips |
| FastFileCompilerGUI/FastFileCompilerGUI.csproj | Bumped version to 3.1.0 |
Files not reviewed (3)
- Call of Duty FastFile Editor/UI/MainWindowForm.Designer.cs: Language not supported
- FastFileCompilerGUI/MainForm.Designer.cs: Language not supported
- FastFileToolGUI/MainForm.Designer.cs: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| hashTableAndAuth = new byte[FastFileConstants.Xbox360SignedHashTableSize]; | ||
| using var origReader = new BinaryReader(File.OpenRead(originalFfPath)); | ||
| origReader.BaseStream.Seek(FastFileConstants.Xbox360SignedHashTableStart, SeekOrigin.Begin); | ||
| origReader.Read(hashTableAndAuth, 0, hashTableAndAuth.Length); |
There was a problem hiding this comment.
The BinaryReader.Read method is used here, but its return value (number of bytes actually read) is not checked. If the file is shorter than expected or if an I/O error occurs, fewer bytes than requested might be read, leading to incomplete or corrupted hash table data. Consider using ReadBytes or checking the return value to ensure all expected bytes were read.
| origReader.Read(hashTableAndAuth, 0, hashTableAndAuth.Length); | |
| int totalRead = 0; | |
| while (totalRead < hashTableAndAuth.Length) | |
| { | |
| int bytesRead = origReader.Read(hashTableAndAuth, totalRead, hashTableAndAuth.Length - totalRead); | |
| if (bytesRead == 0) | |
| { | |
| break; | |
| } | |
| totalRead += bytesRead; | |
| } | |
| if (totalRead != hashTableAndAuth.Length) | |
| { | |
| // Incomplete hash table read; fall back to zeroed hash table behavior. | |
| hashTableAndAuth = null; | |
| } |
| { | ||
| using var origReader = new BinaryReader(File.OpenRead(_originalFfPath)); | ||
| origReader.BaseStream.Seek(FastFileConstants.Xbox360SignedHashTableStart, SeekOrigin.Begin); | ||
| origReader.Read(hashTableAndAuth, 0, hashTableAndAuth.Length); |
There was a problem hiding this comment.
The BinaryReader.Read method is used here, but its return value (number of bytes actually read) is not checked. If the file is shorter than expected or if an I/O error occurs, fewer bytes than requested might be read, leading to incomplete or corrupted hash table data. Consider using ReadBytes or checking the return value to ensure all expected bytes were read.
| origReader.Read(hashTableAndAuth, 0, hashTableAndAuth.Length); | |
| byte[] readBytes = origReader.ReadBytes(hashTableAndAuth.Length); | |
| Array.Copy(readBytes, 0, hashTableAndAuth, 0, readBytes.Length); |
|
|
||
| /// <summary> | ||
| /// Compresses a zone file to a FastFile with Xbox 360 signed format. | ||
| /// Xbox 360 signed files use IWffs100 streaming format with a single zlib stream. |
There was a problem hiding this comment.
This comment describes the streaming header as "IWffs100", but according to the earlier documentation comment (line 902), Xbox 360 signed files use "IWff0100" (signed header), not "IWffs100" (streaming header). The streaming header is a separate 8-byte header written after the main signed header. The comment should clarify that Xbox 360 signed files use both IWff0100 and IWffs100 headers in sequence.
| /// Xbox 360 signed files use IWffs100 streaming format with a single zlib stream. | |
| /// Xbox 360 signed files use an IWff0100 signed header followed by an IWffs100 streaming header, | |
| /// with a single zlib stream. |
| } | ||
|
|
||
| // Read entire zone file and compress as single stream | ||
| byte[] zoneData = br.ReadBytes((int)br.BaseStream.Length); |
There was a problem hiding this comment.
Reading the entire file into memory using ReadBytes with BaseStream.Length cast to int could cause issues with large files. If the zone file is larger than 2GB (Int32.MaxValue), the cast will overflow and cause incorrect behavior. Additionally, for very large files, this could cause OutOfMemoryException. Consider either checking the file size first, using a streaming approach, or documenting the maximum supported file size.
| byte[] zoneData = br.ReadBytes((int)br.BaseStream.Length); | |
| long zoneLength = br.BaseStream.Length; | |
| if (zoneLength > int.MaxValue) | |
| { | |
| throw new IOException("Zone file is too large to process. Maximum supported size is 2GB."); | |
| } | |
| byte[] zoneData = br.ReadBytes((int)zoneLength); |
| } | ||
|
|
||
| // Read entire zone file and compress as single stream | ||
| byte[] zoneData = br.ReadBytes((int)br.BaseStream.Length); |
There was a problem hiding this comment.
Reading the entire file into memory using ReadBytes with BaseStream.Length cast to int could cause issues with large files. If the zone file is larger than 2GB (Int32.MaxValue), the cast will overflow and cause incorrect behavior. Additionally, for very large files, this could cause OutOfMemoryException. Consider either checking the file size first, using a streaming approach, or documenting the maximum supported file size.
| byte[] zoneData = br.ReadBytes((int)br.BaseStream.Length); | |
| long zoneLength = br.BaseStream.Length; | |
| if (zoneLength > int.MaxValue) | |
| { | |
| throw new IOException("Zone file is too large to be processed (maximum supported size is 2GB)."); | |
| } | |
| byte[] zoneData = br.ReadBytes((int)zoneLength); |
| hashTableAndAuth = new byte[FastFileConstants.Xbox360SignedHashTableSize]; | ||
| using var origReader = new BinaryReader(File.OpenRead(originalFfPath)); | ||
| origReader.BaseStream.Seek(FastFileConstants.Xbox360SignedHashTableStart, SeekOrigin.Begin); | ||
| origReader.Read(hashTableAndAuth, 0, hashTableAndAuth.Length); |
There was a problem hiding this comment.
The BinaryReader.Read method is used here, but its return value (number of bytes actually read) is not checked. If the file is shorter than expected or if an I/O error occurs, fewer bytes than requested might be read, leading to incomplete or corrupted hash table data. Consider using ReadBytes or checking the return value to ensure all expected bytes were read.
| origReader.Read(hashTableAndAuth, 0, hashTableAndAuth.Length); | |
| int bytesRead = origReader.Read(hashTableAndAuth, 0, hashTableAndAuth.Length); | |
| if (bytesRead != hashTableAndAuth.Length) | |
| { | |
| throw new EndOfStreamException("Unexpected end of original FF file while reading hash table and auth data."); | |
| } |
| if (xbox360Signed && originalFfPath != null) | ||
| { | ||
| FastFileProcessor.CompressXbox360Signed(packInputTextBox.Text, packOutputTextBox.Text, gameVersion, originalFfPath); | ||
| } | ||
| else | ||
| { | ||
| Compress(packInputTextBox.Text, packOutputTextBox.Text, gameVersion, platform); | ||
| } |
There was a problem hiding this comment.
The condition checks both xbox360Signed and originalFfPath != null, but at this point in the code flow, if xbox360Signed is true, originalFfPath is guaranteed to be non-null due to the validation at lines 173-178 (which returns early if the user cancels). The null check is redundant and could be simplified to just if (xbox360Signed), or the originalFfPath parameter in the method call could use the null-forgiving operator since we know it's non-null here.
| finally | ||
| { | ||
| // Clean up temp file | ||
| try { File.Delete(tempZonePath); } catch { } |
There was a problem hiding this comment.
The empty catch block silently swallows any exceptions that occur during temp file deletion. While this might be intentional to prevent cleanup failures from affecting the overall operation, it could hide legitimate issues (e.g., permissions problems, disk full). Consider at minimum logging the exception or using a more specific exception filter to only catch expected exceptions like IOException or UnauthorizedAccessException.
| try { File.Delete(tempZonePath); } catch { } | |
| try | |
| { | |
| File.Delete(tempZonePath); | |
| } | |
| catch (IOException ex) | |
| { | |
| System.Diagnostics.Debug.WriteLine($"Failed to delete temp file '{tempZonePath}': {ex}"); | |
| } | |
| catch (UnauthorizedAccessException ex) | |
| { | |
| System.Diagnostics.Debug.WriteLine($"Failed to delete temp file '{tempZonePath}': {ex}"); | |
| } |
| /// <summary> | ||
| /// Compresses a zone file to a FastFile with Xbox 360 signed format. | ||
| /// Xbox 360 signed files use IWffs100 streaming format with a single zlib stream. | ||
| /// </summary> | ||
| /// <param name="inputPath">Path to the .zone file</param> | ||
| /// <param name="outputPath">Path to output the .ff file</param> | ||
| /// <param name="gameVersion">Target game version</param> | ||
| /// <param name="originalFfPath">Path to original FF file (to preserve hash table)</param> | ||
| /// <returns>1 (single stream compressed)</returns> | ||
| public static int CompressXbox360Signed(string inputPath, string outputPath, GameVersion gameVersion, string originalFfPath) | ||
| { | ||
| // Read hash table from original file before opening output | ||
| byte[] hashTableAndAuth = null; | ||
| if (!string.IsNullOrEmpty(originalFfPath) && File.Exists(originalFfPath)) | ||
| { | ||
| hashTableAndAuth = new byte[FastFileConstants.Xbox360SignedHashTableSize]; | ||
| using var origReader = new BinaryReader(File.OpenRead(originalFfPath)); | ||
| origReader.BaseStream.Seek(FastFileConstants.Xbox360SignedHashTableStart, SeekOrigin.Begin); | ||
| origReader.Read(hashTableAndAuth, 0, hashTableAndAuth.Length); | ||
| } | ||
|
|
||
| using var br = new BinaryReader(new FileStream(inputPath, FileMode.Open, FileAccess.Read), Encoding.Default); | ||
| using var bw = new BinaryWriter(new FileStream(outputPath, FileMode.Create, FileAccess.Write), Encoding.Default); | ||
|
|
||
| // Write signed header (IWff0100) | ||
| bw.Write(FastFileConstants.SignedHeaderBytes); | ||
|
|
||
| // Write version (big-endian) | ||
| bw.Write(FastFileInfo.GetVersionBytes(gameVersion, "Xbox360")); | ||
|
|
||
| // Write streaming header (IWffs100) | ||
| bw.Write(FastFileConstants.StreamingHeaderBytes); | ||
|
|
||
| // Write hash table and auth data (preserved from original or zeros) | ||
| if (hashTableAndAuth != null) | ||
| { | ||
| bw.Write(hashTableAndAuth); | ||
| } | ||
| else | ||
| { | ||
| bw.Write(new byte[FastFileConstants.Xbox360SignedHashTableSize]); | ||
| } | ||
|
|
||
| // Read entire zone file and compress as single stream | ||
| byte[] zoneData = br.ReadBytes((int)br.BaseStream.Length); | ||
| byte[] compressedData = CompressFullZlib(zoneData); | ||
| bw.Write(compressedData); | ||
|
|
||
| // No end marker for signed format | ||
| return 1; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Compresses a zone file to a FastFile with Xbox 360 signed format using provided version bytes. | ||
| /// </summary> | ||
| /// <param name="inputPath">Path to the .zone file</param> | ||
| /// <param name="outputPath">Path to output the .ff file</param> | ||
| /// <param name="versionBytes">Version bytes (4 bytes, big-endian)</param> | ||
| /// <param name="originalFfPath">Path to original FF file (to preserve hash table)</param> | ||
| /// <returns>1 (single stream compressed)</returns> | ||
| public static int CompressXbox360Signed(string inputPath, string outputPath, byte[] versionBytes, string originalFfPath) | ||
| { | ||
| // Read hash table from original file before opening output | ||
| byte[] hashTableAndAuth = null; | ||
| if (!string.IsNullOrEmpty(originalFfPath) && File.Exists(originalFfPath)) | ||
| { | ||
| hashTableAndAuth = new byte[FastFileConstants.Xbox360SignedHashTableSize]; | ||
| using var origReader = new BinaryReader(File.OpenRead(originalFfPath)); | ||
| origReader.BaseStream.Seek(FastFileConstants.Xbox360SignedHashTableStart, SeekOrigin.Begin); | ||
| origReader.Read(hashTableAndAuth, 0, hashTableAndAuth.Length); | ||
| } | ||
|
|
||
| using var br = new BinaryReader(new FileStream(inputPath, FileMode.Open, FileAccess.Read), Encoding.Default); | ||
| using var bw = new BinaryWriter(new FileStream(outputPath, FileMode.Create, FileAccess.Write), Encoding.Default); | ||
|
|
||
| // Write signed header (IWff0100) | ||
| bw.Write(FastFileConstants.SignedHeaderBytes); | ||
|
|
||
| // Write version bytes | ||
| bw.Write(versionBytes); | ||
|
|
||
| // Write streaming header (IWffs100) | ||
| bw.Write(FastFileConstants.StreamingHeaderBytes); | ||
|
|
||
| // Write hash table and auth data (preserved from original or zeros) | ||
| if (hashTableAndAuth != null) | ||
| { | ||
| bw.Write(hashTableAndAuth); | ||
| } | ||
| else | ||
| { | ||
| bw.Write(new byte[FastFileConstants.Xbox360SignedHashTableSize]); | ||
| } | ||
|
|
||
| // Read entire zone file and compress as single stream | ||
| byte[] zoneData = br.ReadBytes((int)br.BaseStream.Length); | ||
| byte[] compressedData = CompressFullZlib(zoneData); | ||
| bw.Write(compressedData); | ||
|
|
||
| // No end marker for signed format | ||
| return 1; | ||
| } |
There was a problem hiding this comment.
These two overloads of CompressXbox360Signed contain nearly identical code with only a 2-line difference (lines 928 vs 979). This duplication makes maintenance harder and increases the risk of bugs when updating one version but forgetting the other. Consider refactoring by having the GameVersion overload call the byte[] version after converting the GameVersion to version bytes, which would eliminate the duplication.
| var fastFile = new List<byte>(); | ||
|
|
||
| // Write signed header (IWff0100) | ||
| fastFile.AddRange(FastFileConstants.SignedHeaderBytes); | ||
|
|
||
| // Write version (big-endian) - use Xbox360 platform version | ||
| fastFile.AddRange(FastFileInfo.GetVersionBytes(_gameVersion, "Xbox360")); | ||
|
|
||
| // Write streaming header (IWffs100) | ||
| fastFile.AddRange(FastFileConstants.StreamingHeaderBytes); | ||
|
|
||
| // Read and write hash table from original file (or zeros if not available) | ||
| byte[] hashTableAndAuth = new byte[FastFileConstants.Xbox360SignedHashTableSize]; | ||
| if (!string.IsNullOrEmpty(_originalFfPath) && File.Exists(_originalFfPath)) | ||
| { | ||
| using var origReader = new BinaryReader(File.OpenRead(_originalFfPath)); | ||
| origReader.BaseStream.Seek(FastFileConstants.Xbox360SignedHashTableStart, SeekOrigin.Begin); | ||
| origReader.Read(hashTableAndAuth, 0, hashTableAndAuth.Length); | ||
| } | ||
| fastFile.AddRange(hashTableAndAuth); | ||
|
|
||
| // Compress entire zone as single zlib stream | ||
| byte[] compressedData = FastFileProcessor.CompressFullZlib(zoneData); | ||
| fastFile.AddRange(compressedData); | ||
|
|
||
| // No end marker for signed format | ||
| return fastFile.ToArray(); |
There was a problem hiding this comment.
The CompileXbox360Signed method in Compiler.cs (lines 82-111) duplicates nearly identical logic to CompressXbox360Signed in FastFileProcessor.cs (lines 900-950). Both methods read the hash table from the original file, write the same headers, and compress the data. Consider having Compiler.CompileXbox360Signed call FastFileProcessor.CompressXbox360Signed with a temporary file, or extract the shared logic into a helper method to avoid this duplication.
| var fastFile = new List<byte>(); | |
| // Write signed header (IWff0100) | |
| fastFile.AddRange(FastFileConstants.SignedHeaderBytes); | |
| // Write version (big-endian) - use Xbox360 platform version | |
| fastFile.AddRange(FastFileInfo.GetVersionBytes(_gameVersion, "Xbox360")); | |
| // Write streaming header (IWffs100) | |
| fastFile.AddRange(FastFileConstants.StreamingHeaderBytes); | |
| // Read and write hash table from original file (or zeros if not available) | |
| byte[] hashTableAndAuth = new byte[FastFileConstants.Xbox360SignedHashTableSize]; | |
| if (!string.IsNullOrEmpty(_originalFfPath) && File.Exists(_originalFfPath)) | |
| { | |
| using var origReader = new BinaryReader(File.OpenRead(_originalFfPath)); | |
| origReader.BaseStream.Seek(FastFileConstants.Xbox360SignedHashTableStart, SeekOrigin.Begin); | |
| origReader.Read(hashTableAndAuth, 0, hashTableAndAuth.Length); | |
| } | |
| fastFile.AddRange(hashTableAndAuth); | |
| // Compress entire zone as single zlib stream | |
| byte[] compressedData = FastFileProcessor.CompressFullZlib(zoneData); | |
| fastFile.AddRange(compressedData); | |
| // No end marker for signed format | |
| return fastFile.ToArray(); | |
| // Delegate to shared implementation in FastFileProcessor to avoid duplication. | |
| return FastFileProcessor.CompressXbox360Signed(zoneData, _gameVersion, _originalFfPath); |
Dev to main v3.1.0