Skip to content

Commit 2b7406a

Browse files
authored
Merge pull request #25 from primetime43/dev
Dev to main v2.1.0
2 parents 0ba6b47 + 3bd4c1c commit 2b7406a

9 files changed

Lines changed: 388 additions & 26 deletions

File tree

.github/workflows/release.yml

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
name: Build and Release
2+
3+
on:
4+
push:
5+
tags:
6+
- 'v*' # Triggers on version tags like v1.0.0, v2.0.0, etc.
7+
workflow_dispatch: # Allows manual trigger from GitHub UI
8+
inputs:
9+
version:
10+
description: 'Version tag (e.g., v2.1.0)'
11+
required: true
12+
type: string
13+
14+
jobs:
15+
build:
16+
runs-on: windows-latest
17+
18+
steps:
19+
- name: Checkout code
20+
uses: actions/checkout@v4
21+
22+
- name: Setup .NET
23+
uses: actions/setup-dotnet@v4
24+
with:
25+
dotnet-version: '8.0.x'
26+
27+
- name: Restore dependencies
28+
run: dotnet restore "Call of Duty FastFile Editor.sln"
29+
30+
- name: Build and Publish
31+
run: |
32+
dotnet publish "Call of Duty FastFile Editor/Call of Duty FastFile Editor.csproj" `
33+
-c Release `
34+
-r win-x64 `
35+
--self-contained false `
36+
-p:PublishSingleFile=true `
37+
-p:IncludeNativeLibrariesForSelfExtract=true `
38+
-o ./publish
39+
40+
- name: List published files
41+
run: Get-ChildItem -Path ./publish -Recurse
42+
43+
- name: Get version from tag
44+
id: get_version
45+
shell: bash
46+
run: |
47+
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
48+
echo "VERSION=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT
49+
else
50+
echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
51+
fi
52+
53+
- name: Create Release
54+
uses: softprops/action-gh-release@v1
55+
with:
56+
tag_name: ${{ steps.get_version.outputs.VERSION }}
57+
name: Release ${{ steps.get_version.outputs.VERSION }}
58+
draft: false
59+
prerelease: false
60+
generate_release_notes: true
61+
files: |
62+
./publish/Call of Duty FastFile Editor.exe
63+
env:
64+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Call of Duty FastFile Editor/Constants/ApplicationConstants.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
public static class ApplicationConstants
44
{
55
public const string ProgramName = "Call of Duty Fast File Editor for PS3";
6-
public const string ProgramVersion = "v2.0.0";
6+
public const string ProgramVersion = "v2.1.0";
77
public const string About = $"{ProgramName}\n" +
88
"Version: " + ProgramVersion + "\n\n" +
99
"Developed by primetime43\n\n" +

Call of Duty FastFile Editor/IO/FastFileHandlerBase.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,8 @@ public void Recompress(string ffFilePath, string zoneFilePath, FastFile openedFa
116116

117117
binaryWriter.Write(compressedChunk);
118118
}
119-
binaryWriter.Write(new byte[2] { 0, 1 });
119+
// Note: No terminator is written. Original FastFiles end after the last
120+
// compressed chunk. The game detects EOF by reading past end of file.
120121
}
121122
}
122123

Call of Duty FastFile Editor/Models/RawFileNode.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ public RawFileNode(string name, byte[] buffer)
6060
public int CodeStartPosition => StartOfFileHeader + 12 + (FileName?.Length ?? 0) + 1;
6161

6262
/// <summary>
63-
/// Gets the position where the code ends, calculated as CodeStartPosition + MaxSize - 1 to not include the null byte.
64-
/// Doesn't includes null byte. This is the offset before the null terminator
63+
/// Gets the position where the code ends, calculated as CodeStartPosition + MaxSize
64+
/// Minus 1 because the last byte is a null terminator.
6565
/// </summary>
6666
public int CodeEndPosition => CodeStartPosition + MaxSize - 1;
6767

@@ -77,10 +77,10 @@ public RawFileNode(string name, byte[] buffer)
7777
public string FileName { get; set; }
7878

7979
/// <summary>
80-
/// Gets the position where the code ends, calculated as CodeStartPosition + MaxSize + 1 for the null byte.
81-
/// Includes null byte. This is the offset after the null terminator
80+
/// Gets the position where the code ends, calculated as CodeStartPosition + MaxSize
81+
/// This is where the null terminator is at the end of the asset
8282
/// </summary>
83-
public int RawFileEndPosition => CodeStartPosition + MaxSize + 1;
83+
public int RawFileEndPosition => CodeStartPosition + MaxSize;
8484

8585
/// <summary>
8686
/// The content of the file as a string.

Call of Duty FastFile Editor/Services/IO/ZoneFileIO.cs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,43 @@ public static uint ReadZoneFileSize(string path)
2323

2424
/// <summary>
2525
/// Writes the updated zone file size (big-endian) to the header at the defined offset.
26+
/// Also updates the EndOfFileDataPointer to stay in sync.
2627
/// </summary>
2728
public static void WriteZoneFileSize(string path, uint newSize)
2829
{
30+
// Read the current EndOfFileDataPointer to calculate the offset from FileSize
31+
uint currentFileSize = ReadZoneFileSize(path);
32+
uint currentEndPointer = ReadEndOfFileDataPointer(path);
33+
34+
// Calculate the difference between EndOfFileDataPointer and FileSize
35+
// This offset should remain constant when we update the size
36+
uint pointerOffset = currentEndPointer - currentFileSize;
37+
uint newEndPointer = newSize + pointerOffset;
38+
2939
Span<byte> b = stackalloc byte[4];
30-
System.Buffers.Binary.BinaryPrimitives.WriteUInt32BigEndian(b, newSize);
3140
using var fs = new FileStream(path, FileMode.Open, FileAccess.Write);
41+
42+
// Write the new FileSize
43+
System.Buffers.Binary.BinaryPrimitives.WriteUInt32BigEndian(b, newSize);
3244
fs.Seek(ZoneFileHeaderConstants.ZoneSizeOffset, SeekOrigin.Begin);
3345
fs.Write(b);
46+
47+
// Write the new EndOfFileDataPointer
48+
System.Buffers.Binary.BinaryPrimitives.WriteUInt32BigEndian(b, newEndPointer);
49+
fs.Seek(ZoneFileHeaderConstants.EndOfFileDataPointer, SeekOrigin.Begin);
50+
fs.Write(b);
51+
}
52+
53+
/// <summary>
54+
/// Reads the EndOfFileDataPointer from the zone header (big-endian).
55+
/// </summary>
56+
public static uint ReadEndOfFileDataPointer(string path)
57+
{
58+
var b = new byte[4];
59+
using var fs = new FileStream(path, FileMode.Open, FileAccess.Read);
60+
fs.Seek(ZoneFileHeaderConstants.EndOfFileDataPointer, SeekOrigin.Begin);
61+
fs.Read(b, 0, 4);
62+
return System.Buffers.Binary.BinaryPrimitives.ReadUInt32BigEndian(b);
3463
}
3564
}
3665
}

Call of Duty FastFile Editor/Services/IRawFileService.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,19 @@ public interface IRawFileService
99
{
1010
/// <summary>
1111
/// Exports the given raw file node to an external file with the specified extension.
12+
/// Includes the zone header for re-injection compatibility.
1213
/// </summary>
1314
/// <param name="node">The raw file node to export.</param>
1415
/// <param name="extension">The file extension to use for the exported file (including the leading dot).</param>
1516
void ExportRawFile(RawFileNode node, string extension);
1617

18+
/// <summary>
19+
/// Exports only the content of the raw file (without zone header) for external editing.
20+
/// </summary>
21+
/// <param name="node">The raw file node to export.</param>
22+
/// <param name="extension">The file extension to use for the exported file.</param>
23+
void ExportRawFileContentOnly(RawFileNode node, string extension);
24+
1725
/// <summary>
1826
/// Overwrites the content of a raw file inside the zone file, padding or trimming as needed.
1927
/// </summary>
@@ -64,12 +72,21 @@ void RenameRawFile(
6472

6573
/// <summary>
6674
/// Appends a brand‑new raw file entry to the end of the asset pool in the zone file.
75+
/// The file must include the zone header (FF FF FF FF markers).
6776
/// </summary>
6877
/// <param name="zoneFilePath">Path to the decompressed Zone File (.zone).</param>
6978
/// <param name="filePath">Path to the external file to inject (must include its own header).</param>
7079
/// <param name="expectedSize">The expected data size for this entry.</param>
7180
void AppendNewRawFile(string zoneFilePath, string filePath, int expectedSize);
7281

82+
/// <summary>
83+
/// Injects a plain file (without zone header) by creating the header structure.
84+
/// </summary>
85+
/// <param name="zoneFilePath">Path to the decompressed Zone File (.zone).</param>
86+
/// <param name="filePath">Path to the plain file to inject.</param>
87+
/// <param name="gamePath">The game path for this file (e.g., "maps/mp/gametypes/dm.gsc").</param>
88+
void InjectPlainFile(string zoneFilePath, string filePath, string gamePath);
89+
7390
/// <summary>
7491
/// Adjusts the maximum size of a raw file node by padding with zeros or shifting data.
7592
/// </summary>

Call of Duty FastFile Editor/Services/RawFileService.cs

Lines changed: 144 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,81 @@ public void AppendNewRawFile(string zoneFilePath, string filePath, int expectedS
4444
currentZone.ReadHeaderFields();
4545
}
4646

47+
/// <inheritdoc/>
48+
public void InjectPlainFile(string zoneFilePath, string filePath, string gamePath)
49+
{
50+
// Read the plain file content
51+
byte[] fileContent = File.ReadAllBytes(filePath);
52+
int contentSize = fileContent.Length;
53+
54+
// Build the raw file entry with header:
55+
// - 4 bytes: first marker (0xFFFFFFFF)
56+
// - 4 bytes: data size (big-endian)
57+
// - 4 bytes: second marker (0xFFFFFFFF)
58+
// - N bytes: filename + null terminator
59+
// - M bytes: file content
60+
byte[] fileNameBytes = Encoding.ASCII.GetBytes(gamePath);
61+
int headerSize = 12 + fileNameBytes.Length + 1; // 12 bytes markers/size + filename + null
62+
int totalSize = headerSize + contentSize;
63+
64+
byte[] newEntry = new byte[totalSize];
65+
66+
// Write first marker (0xFFFFFFFF)
67+
newEntry[0] = 0xFF;
68+
newEntry[1] = 0xFF;
69+
newEntry[2] = 0xFF;
70+
newEntry[3] = 0xFF;
71+
72+
// Write data size (big-endian)
73+
System.Buffers.Binary.BinaryPrimitives.WriteUInt32BigEndian(
74+
newEntry.AsSpan(4, 4),
75+
(uint)contentSize
76+
);
77+
78+
// Write second marker (0xFFFFFFFF)
79+
newEntry[8] = 0xFF;
80+
newEntry[9] = 0xFF;
81+
newEntry[10] = 0xFF;
82+
newEntry[11] = 0xFF;
83+
84+
// Write filename
85+
Array.Copy(fileNameBytes, 0, newEntry, 12, fileNameBytes.Length);
86+
// Null terminator is already 0x00 from array initialization
87+
88+
// Write content
89+
Array.Copy(fileContent, 0, newEntry, headerSize, contentSize);
90+
91+
// Now inject the entry into the zone file
92+
ZoneFile currentZone = RawFileNode.CurrentZone;
93+
int insertPosition = currentZone.AssetPoolEndOffset;
94+
95+
currentZone.ModifyZoneFile(fs =>
96+
{
97+
long originalLength = fs.Length;
98+
// Read tail data from the insertion point.
99+
fs.Seek(insertPosition, SeekOrigin.Begin);
100+
byte[] tailBuffer = new byte[originalLength - insertPosition];
101+
fs.Read(tailBuffer, 0, tailBuffer.Length);
102+
// Extend the file length.
103+
fs.SetLength(originalLength + newEntry.Length);
104+
// Shift tail data forward.
105+
fs.Seek(insertPosition + newEntry.Length, SeekOrigin.Begin);
106+
fs.Write(tailBuffer, 0, tailBuffer.Length);
107+
// Write the new entry.
108+
fs.Seek(insertPosition, SeekOrigin.Begin);
109+
fs.Write(newEntry, 0, newEntry.Length);
110+
});
111+
112+
// Update the zone file size header.
113+
uint currentZoneSize = ZoneFileIO.ReadZoneFileSize(zoneFilePath);
114+
uint newZoneSize = currentZoneSize + (uint)newEntry.Length;
115+
ZoneFileIO.WriteZoneFileSize(zoneFilePath, newZoneSize);
116+
117+
// Refresh zone data and header.
118+
currentZone.LoadData();
119+
currentZone.ReadHeaderFields();
120+
}
121+
47122
/// <inheritdoc/>
48123
public void AdjustRawFileNodeSize(string zoneFilePath, RawFileNode rawFileNode, int newSize)
49124
{
@@ -105,10 +180,14 @@ public void AdjustRawFileNodeSize(string zoneFilePath, RawFileNode rawFileNode,
105180
uint currentZoneSize = ZoneFileIO.ReadZoneFileSize(zoneFilePath);
106181
uint updatedZoneSize = currentZoneSize + (uint)sizeIncrease;
107182
ZoneFileIO.WriteZoneFileSize(zoneFilePath, updatedZoneSize);
183+
184+
// Refresh zone header fields after modification.
185+
currentZone.LoadData();
186+
currentZone.ReadHeaderFields();
108187
}
109188

110189
/// <summary>
111-
/// Adjusts a raw file entry read from disk so that its headers size field (at offset 4)
190+
/// Adjusts a raw file entry read from disk so that its header's size field (at offset 4)
112191
/// matches the expected data size. It uses the known header structure:
113192
/// Bytes 0-3: first marker (0xFFFFFFFF)
114193
/// Bytes 4-7: data size (to be updated)
@@ -179,7 +258,7 @@ public void ExportRawFile(RawFileNode exportedRawFile, string fileExtension)
179258
{
180259
using var save = new SaveFileDialog
181260
{
182-
Title = "Export File",
261+
Title = "Export File (With Header for Re-injection)",
183262
FileName = SanitizeFileName(exportedRawFile.FileName),
184263
Filter = $"{fileExtension.TrimStart('.').ToUpper()} Files (*{fileExtension})|*{fileExtension}|All Files (*.*)|*.*"
185264
};
@@ -197,7 +276,47 @@ public void ExportRawFile(RawFileNode exportedRawFile, string fileExtension)
197276
File.WriteAllBytes(save.FileName, slice);
198277

199278
MessageBox.Show(
200-
$"File successfully exported to:\n\n{save.FileName}",
279+
$"File successfully exported to:\n\n{save.FileName}\n\n" +
280+
"Note: This file includes the zone header and can be re-injected.",
281+
"Export Complete",
282+
MessageBoxButtons.OK,
283+
MessageBoxIcon.Information
284+
);
285+
}
286+
catch (Exception ex)
287+
{
288+
MessageBox.Show(
289+
$"Failed to export file: {ex.Message}",
290+
"Export Error",
291+
MessageBoxButtons.OK,
292+
MessageBoxIcon.Error
293+
);
294+
}
295+
}
296+
297+
/// <inheritdoc/>
298+
public void ExportRawFileContentOnly(RawFileNode exportedRawFile, string fileExtension)
299+
{
300+
using var save = new SaveFileDialog
301+
{
302+
Title = "Export Content Only",
303+
FileName = SanitizeFileName(exportedRawFile.FileName),
304+
Filter = $"{fileExtension.TrimStart('.').ToUpper()} Files (*{fileExtension})|*{fileExtension}|All Files (*.*)|*.*"
305+
};
306+
307+
if (save.ShowDialog() != DialogResult.OK)
308+
return;
309+
310+
try
311+
{
312+
// Export only the actual content (RawFileBytes), not the header
313+
byte[] contentOnly = exportedRawFile.RawFileBytes;
314+
315+
File.WriteAllBytes(save.FileName, contentOnly);
316+
317+
MessageBox.Show(
318+
$"File content successfully exported to:\n\n{save.FileName}\n\n" +
319+
"Note: This file contains only the script content without zone header.",
201320
"Export Complete",
202321
MessageBoxButtons.OK,
203322
MessageBoxIcon.Information
@@ -242,6 +361,12 @@ public void IncreaseSize(string zoneFilePath, RawFileNode rawFileNode, byte[] ne
242361
}
243362
fs.Seek(rawFileNode.CodeStartPosition, SeekOrigin.Begin);
244363
fs.Write(newContent, 0, newSize);
364+
365+
// Update the size field in the raw file header (4 bytes at StartOfFileHeader + 4)
366+
Span<byte> sizeBuf = stackalloc byte[4];
367+
System.Buffers.Binary.BinaryPrimitives.WriteUInt32BigEndian(sizeBuf, (uint)newSize);
368+
fs.Seek(rawFileNode.StartOfFileHeader + 4, SeekOrigin.Begin);
369+
fs.Write(sizeBuf);
245370
});
246371

247372
rawFileNode.MaxSize = newSize;
@@ -251,6 +376,10 @@ public void IncreaseSize(string zoneFilePath, RawFileNode rawFileNode, byte[] ne
251376
uint currentZoneSize = ZoneFileIO.ReadZoneFileSize(zoneFilePath);
252377
uint newZoneSize = currentZoneSize + (uint)sizeIncrease;
253378
ZoneFileIO.WriteZoneFileSize(zoneFilePath, newZoneSize);
379+
380+
// Refresh zone header fields after modification
381+
currentZone.LoadData();
382+
currentZone.ReadHeaderFields();
254383
}
255384

256385
/// <inheritdoc/>
@@ -331,6 +460,18 @@ public void RenameRawFile(TreeView filesTreeView, string ffFilePath, string zone
331460
// Write the modified zone file back to disk.
332461
File.WriteAllBytes(zoneFilePath, zoneFileData);
333462

463+
// Update the zone file size header if the filename length changed.
464+
if (byteDifference != 0)
465+
{
466+
uint currentZoneSize = ZoneFileIO.ReadZoneFileSize(zoneFilePath);
467+
uint newZoneSize = (uint)((int)currentZoneSize + byteDifference);
468+
ZoneFileIO.WriteZoneFileSize(zoneFilePath, newZoneSize);
469+
470+
// Refresh zone data and header fields.
471+
RawFileNode.CurrentZone.LoadData();
472+
RawFileNode.CurrentZone.ReadHeaderFields();
473+
}
474+
334475
// Save the old file name for notification.
335476
string oldFileName = rawFileNode.FileName;
336477
// Update the renamed file's FileName property.

0 commit comments

Comments
 (0)