/
dat.cs
337 lines (288 loc) · 10.6 KB
/
dat.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
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace undat_ui
{
// Implemented by following https://falloutmods.fandom.com/wiki/DAT_file_format#Fallout_1_LZSS_uncompression_algorithm
// https://en.wikipedia.org/wiki/Lempel-Ziv-Storer-Szymanski
// https://web.archive.org/web/20160110174426/https://oku.edu.mie-u.ac.jp/~okumura/compression/history.html
class FO1LZSS
{
int uncompressedSize;
BinaryBigEndian stream;
readonly short DICT_SIZE = 4096;
readonly short MIN_MATCH = 3;
readonly short MAX_MATCH = 18;
byte[] output;
byte[] dictionary;
short N; // number of bytes to read
short NR; // bytes read from last block header
short DO = 0; // dictionary offset - for reading
short DI; // dictionary index - for writing
int OI = 0; // output index, used for writing to the output array.
int L; // match length
byte FL; // Flags indicating the compression status of up to 8 next characters.
public FO1LZSS(BinaryBigEndian stream, int uncompressedSize)
{
this.uncompressedSize = uncompressedSize;
this.stream = stream;
}
bool LastByte() { return stream.BaseStream.Position == stream.BaseStream.Length; }
private void ClearDict()
{
for (var i = 0; i < DICT_SIZE; i++)
dictionary[i] = 0x20; // ' ' in ascii
DI = (short)(DICT_SIZE - MAX_MATCH);
}
private byte ReadByte()
{
NR++;
return stream.ReadByte();
}
private byte[] ReadBytes(int bytes)
{
return stream.ReadBytes(bytes);
}
// Write to output and dictionary
void WriteByte(byte b)
{
output[OI++] = b;
dictionary[(DI++ % DICT_SIZE)] = b;
}
private void WriteBytes(byte[] bytes)
{
foreach (var b in bytes)
{
if (OI >= uncompressedSize)
return;
output[OI++] = b;
}
}
private byte ReadDict()
{
return dictionary[(DO++ % DICT_SIZE)];
}
private Int16 ReadInt16()
{
return stream.ReadInt16();
}
private void ReadBlock(int N)
{
NR = 0; // The amount of bytes we have read in the current block so far
if (N < 0) // Uncompressed / literal block
{
WriteBytes(ReadBytes(N * -1)); // We just read N bytes and write to the output buffer. Dictionary is untouched.
}
else // N > 0
{
ClearDict();
// @Flag
while (true)
{
if (NR >= N || LastByte())
return; // Go to @Start
// Read flag byte
FL = ReadByte();
if (NR >= N || LastByte())
return; // Go to @Start
for (var x = 0; x < 8; x++)
{
if (FL % 2 == 1) // @FLodd, normal byte
{
// Read byte from stream and put it in the output buffer and dictionary.
WriteByte(ReadByte());
if (NR >= N)
return;
}
else // @FLeven, encoded dictionary offset
{
if (NR >= N)
return;
// Read dictionary offset byte
DO = ReadByte();
if (NR >= N)
return;
// Length byte
var LB = ReadByte();
DO |= (short)((LB & 0xF0) << 4); // Prepend the high-nibble (first 4 bits) from LB to DO
L = (int)((LB & 0x0F) + MIN_MATCH); // and remove it from LB and add MIN_MATCH
for (var i = 0; i < L; i++)
{
// Read a byte from the dictionary at DO, increment index and write to output and dictionary at DI.
WriteByte(ReadDict());
}
}
FL = (byte)(FL >> 1); // @flagNext
if (LastByte())
return;
}
}
}
}
public byte[] Decompress() {
output = new byte[uncompressedSize];
dictionary = new byte[DICT_SIZE];
while(!LastByte()) // @Start
{
N = ReadInt16(); // How many bytes to read in the next block
if (N == 0) // No bytes, so exit
break;
ReadBlock(N);
}
return output;
}
}
class BinaryBigEndian : BinaryReader
{
public BinaryBigEndian(System.IO.Stream stream) : base(stream) { }
public override int ReadInt32()
{
var data = base.ReadBytes(4);
Array.Reverse(data);
return BitConverter.ToInt32(data, 0);
}
public override Int16 ReadInt16()
{
var data = base.ReadBytes(2);
Array.Reverse(data);
return BitConverter.ToInt16(data, 0);
}
public override Int64 ReadInt64()
{
var data = base.ReadBytes(8);
Array.Reverse(data);
return BitConverter.ToInt64(data, 0);
}
public override UInt32 ReadUInt32()
{
var data = base.ReadBytes(4);
Array.Reverse(data);
return BitConverter.ToUInt32(data, 0);
}
}
public enum ReadError
{
Unknown,
FileDoesntExist,
NotValidMasterDat,
Success
}
public class FO1File
{
public string fullPath;
public string name;
public bool packed;
public int offset;
public int size;
public int packedSize;
public byte[] getData(FileStream stream)
{
return packed ? getCompressedBytes(stream) : getBytes(stream);
}
public byte[] getBytes(FileStream stream)
{
var r = new BinaryBigEndian(stream);
r.BaseStream.Seek(offset, SeekOrigin.Begin);
return r.ReadBytes(size);
}
public byte[] getCompressedBytes(FileStream stream)
{
// Create a new stream so that we are thread safe.
var s = new MemoryStream();
BinaryBigEndian r;
stream.Seek(offset, SeekOrigin.Begin);
// Copy packedSize amount of bytes from the original dat stream to our new memory stream.
for (int i = 0; i < packedSize; i++)
s.WriteByte((byte)stream.ReadByte());
r = new BinaryBigEndian(s);
r.BaseStream.Seek(0, SeekOrigin.Begin);
var LZSS = new FO1LZSS(r, size);
var bytes = LZSS.Decompress();
s.Dispose();
r.Dispose();
GC.Collect();
return bytes;
}
}
public class FO1Dir
{
public string name;
public int fileCount;
public List<FO1File> files = new List<FO1File>();
}
// https://falloutmods.fandom.com/wiki/DAT_file_format
public class FO1Dat
{
int dirCount; // number of directories
int unknown; // Usually 0x0A (0x5E for master.dat)
int unknown2; // Always 0
int unknown3; // Checksum?
FileStream fileStream;
List<FO1Dir> directories;
public byte[] getData(FO1File file)
{
return file.getData(fileStream);
}
public FO1File getFile(string path)
{
var dir = path.Split('\\').ToList();
var file = dir.Last();
dir.Remove(dir.Last());
var found = directories.Where(x => x.name == string.Join("\\", dir)).SingleOrDefault();
if (found == null)
return null;
return found.files.Where(x => x.name == file).SingleOrDefault();
}
private string ReadString(BinaryBigEndian r)
{
var len = r.ReadByte();
return Encoding.ASCII.GetString(r.ReadBytes(len));
}
public ReadError Open(string filename)
{
if (!File.Exists(filename))
return ReadError.FileDoesntExist;
directories = new List<FO1Dir>();
fileStream = File.OpenRead(filename);
var r = new BinaryBigEndian(fileStream);
dirCount = r.ReadInt32();
unknown = r.ReadInt32();
unknown2 = r.ReadInt32();
unknown3 = r.ReadInt32();
if(unknown != 0x5E || unknown2 != 0)
return ReadError.NotValidMasterDat;
for (var i = 0; i < dirCount; i++)
{
directories.Add(new FO1Dir() { name = ReadString(r) });
}
for (var i=0; i < dirCount;i++)
{
var dir = directories[i];
dir.fileCount = r.ReadInt32();
r.ReadInt32(); // unknown4
r.ReadInt32(); // unknown5
r.ReadInt32(); // unknown6
for (var y = 0; y < dir.fileCount; y++)
{
var name = ReadString(r);
var packed = r.ReadInt32() == 0x40;
var offset = r.ReadInt32();
var size = r.ReadInt32();
var sizePacked = r.ReadInt32();
dir.files.Add(new FO1File
{
packed = packed,
name = name,
fullPath = dir.name + "\\" + name,
offset = offset,
size = size,
packedSize = sizePacked
});
}
}
return ReadError.Success;
}
}
}