forked from kiraio-moe/UnityWebTools.NET
-
Notifications
You must be signed in to change notification settings - Fork 0
/
UnityWebTool.cs
209 lines (172 loc) · 7.54 KB
/
UnityWebTool.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
using System.Text;
using Kaitai;
namespace Kiraio.UnityWebTools
{
#region UnityWebData File Structure
struct WebData
{
public byte[] Magic;
public uint FirstFileOffset;
public List<FileEntry> FileEntries;
public List<byte[]> FileContents;
}
struct FileEntry
{
public uint FileOffset;
public uint FileSize;
public uint FileNameSize;
public byte[] Name;
}
#endregion
/// <summary>
/// Main class to handle `UnityWebData`.
/// </summary>
public static class UnityWebTool
{
/// <summary>
/// UnityWebData signature
/// </summary>
internal const string MAGIC_HEADER = "UnityWebData1.0";
/// <summary>
/// Unpack UnityWebData (*.data) to files.
/// </summary>
/// <param name="webDataFile">UnityWebData (*.data) file.</param>
/// <param name="outputDirectory">Optional, default to <paramref name="webDataFile"/> name directory in the current working directory.</param>
/// <returns>Output directory.</returns>
public static string Unpack(string? webDataFile, string? outputDirectory = null)
{
if (!File.Exists(webDataFile))
throw new FileNotFoundException($"{webDataFile} didn\'t exist!");
// Set output directory default path
outputDirectory ??= Path.Combine(
Path.GetDirectoryName(webDataFile) ?? string.Empty,
Path.GetFileNameWithoutExtension(webDataFile)
);
if (!Directory.Exists(outputDirectory))
Directory.CreateDirectory(outputDirectory);
// Create the Kaitai stream and the root object from the parsed data
UnityWebData? unityWebData = UnityWebData.FromFile(webDataFile);
foreach (UnityWebData.FileEntry fileEntry in unityWebData.Files)
{
string? fileName = fileEntry?.Filename;
// Create file entry directory
string? fileNameDirectory = Path.Combine(
outputDirectory,
Path.GetDirectoryName(fileName) ?? string.Empty
);
if (!Directory.Exists(fileNameDirectory))
Directory.CreateDirectory(fileNameDirectory);
string? outputFile = Path.Combine(outputDirectory, fileName ?? string.Empty);
using FileStream? outputFileStream = new(outputFile, FileMode.Create);
outputFileStream?.Write(fileEntry?.Data);
}
return outputDirectory;
}
/// <summary>
/// Pack a folder as UnityWebData (*.data) file.
/// </summary>
/// <param name="sourceFolder">The source folder.</param>
/// <param name="outputFile">Optional, default as `<paramref name="sourceFolder"/>_name.data`.</param>
/// <returns>Output file path.</returns>
public static string Pack(string sourceFolder, string? outputFile = null)
{
// Set output file default path
outputFile ??= $"{sourceFolder}.data";
// Get all files recursively
List<string> files = UnityWebToolUtils.GetFilesRecursive(sourceFolder).ToList();
// Get files in root directory by sorting from `files`
List<string> rootFolderFiles = files
.Where(f => Path.GetDirectoryName(f) == sourceFolder)
.ToList();
// Get files inside subdirectories
List<string> subdirectoryFiles = files.Except(rootFolderFiles).ToList();
// Sort the subdirectory files in descending order
subdirectoryFiles.Sort((a, b) => b.CompareTo(a));
// Combine the lists and print the result
files = subdirectoryFiles.Concat(rootFolderFiles).ToList();
List<string>? filesName = new();
foreach (string file in files)
filesName.Add(
file.Replace(sourceFolder, "")
.Trim(Path.DirectorySeparatorChar)
.Replace(@"\", @"/")
);
using MemoryStream tempStream = new();
using BinaryWriter tempWriter = new(tempStream);
byte[] magic = UnityWebToolUtils.AddNullTerminate(Encoding.UTF8.GetBytes(MAGIC_HEADER));
List<FileEntry> fileEntries = new();
List<byte[]> fileContents = new();
List<long> fileOffsetValues = new();
List<long> fileOffsetEntryPosition = new();
// Collect file entries
for (int i = 0; i < files.Count; i++)
{
FileInfo fileInfo = new(files[i]);
byte[] fileNameBytes = Encoding.UTF8.GetBytes(filesName[i]);
fileEntries.Add(
new FileEntry
{
FileOffset = 0,
FileSize = (uint)fileInfo.Length,
FileNameSize = (uint)fileNameBytes.Length,
Name = fileNameBytes
}
);
fileContents.Add(File.ReadAllBytes(files[i]));
}
try
{
WebData webData =
new()
{
Magic = magic,
FirstFileOffset = 0,
FileEntries = fileEntries,
FileContents = fileContents
};
// Write Magic bytes
tempWriter.Write(webData.Magic);
// Write a placeholder for FirstFileOffset
fileOffsetEntryPosition.Add(tempStream.Position);
tempWriter.Write(webData.FirstFileOffset);
// Write each FileEntry
foreach (FileEntry entry in webData.FileEntries)
{
// Write FileOffset
fileOffsetEntryPosition.Add(tempStream.Position);
tempWriter.Write(entry.FileOffset);
// Write FileSize
tempWriter.Write(entry.FileSize);
// Write FileNameSize
tempWriter.Write(entry.FileNameSize);
// Write Name char bytes
tempWriter.Write(entry.Name);
}
foreach (byte[] content in webData.FileContents)
{
// Add current offset to a list to be used later
fileOffsetValues.Add(tempStream.Position);
// Write the actual data
tempWriter.Write(content);
}
// Go back to WebData.FirstFileOffset and write the first file offset
tempStream.Seek(fileOffsetEntryPosition[0], SeekOrigin.Begin);
tempWriter.Write((uint)fileOffsetValues[0]);
// Go back to each FileEntry.FileOffset and write the file offset
for (int i = 0; i < fileOffsetValues.Count; i++)
{
tempStream.Seek(fileOffsetEntryPosition[i + 1], SeekOrigin.Begin);
tempWriter.Write((uint)fileOffsetValues[i]);
}
// Write the entire contents of the temporary stream to the actual file
using FileStream fileStream = new(outputFile, FileMode.Create);
tempStream.WriteTo(fileStream);
}
catch
{
throw new Exception($"Failed to write {outputFile}");
}
return outputFile;
}
}
}