-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathtorchlight2stashconverter.cpp
More file actions
243 lines (195 loc) · 8.69 KB
/
Copy pathtorchlight2stashconverter.cpp
File metadata and controls
243 lines (195 loc) · 8.69 KB
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
#include "torchlight2stashconverter.h"
#include <QFile>
#include <QDebug>
#include <cstring>
using namespace std;
Torchlight2StashConverter::Torchlight2StashConverter(QObject* parent)
: QObject(parent)
{
}
/*
* Torchlight 2 sharedstash v65 checksum calculator proof-of-concept by Shatter
*/
static quint32 CalculateChecksum(const QByteArray& buffer, qint64 offset, qint64 length)
{
quint32 checksum = 0x14d3; // 14d3 seed
for (qint64 i = 0; i < length; ++i)
{
checksum += (checksum << 0x5);
checksum += (quint8)buffer.constData()[offset + i];
}
return checksum;
}
static void Descramble(const QByteArray& inputBuffer, qint64 inputOffset, qint64 length, QByteArray& outputBuffer, qint64 outputOffset)
{
for (int i = 0; i < length; ++i)
{
// get least significant nybble
quint8 leastSignificantNybble = (inputBuffer.constData()[inputOffset + i] & 0xF0) >> 4;
// get most significant nybble
quint8 mostSignificantNybble = inputBuffer.constData()[(inputOffset + (length - 1)) - i] & 0x0F;
// exceptions: descrambled bytes of exactly 0x00 or 0xFF stay that way and do not get inverted!
if (!(((leastSignificantNybble == 0) && (mostSignificantNybble == 0)) ||
((leastSignificantNybble == 0xF) && (mostSignificantNybble == 0xF))))
{
leastSignificantNybble ^= 0xF;
mostSignificantNybble ^= 0xF;
}
outputBuffer.data()[outputOffset + i] = (mostSignificantNybble << 4) | leastSignificantNybble;
}
}
static void Scramble(const QByteArray& inputBuffer, qint32 inputOffset, qint32 length, QByteArray& outputBuffer, qint32 outputOffset)
{
for (qint32 i = 0; i < length; ++i)
{
quint8 leastSignificantNybble = inputBuffer.constData()[inputOffset + i] & 0x0F;
quint8 mostSignificantNybble = (inputBuffer.constData()[inputOffset + i] & 0xF0) >> 4;
if (!((mostSignificantNybble == 0 && leastSignificantNybble == 0) ||
(mostSignificantNybble == 0xF && leastSignificantNybble == 0xF)))
{
leastSignificantNybble ^= 0xF;
mostSignificantNybble ^= 0xF;
}
outputBuffer.data()[outputOffset + i] = (outputBuffer.data()[outputOffset + i] & 0x0F) | (leastSignificantNybble << 4);
outputBuffer.data()[outputOffset + (length - 1) - i] = ((outputBuffer.data()[outputOffset + (length - 1) - i] & 0xF0) | mostSignificantNybble);
}
}
bool Torchlight2StashConverter::DescrambleFile(const QByteArray& inputBuffer, QByteArray& outputBuffer)
{
bool result = false;
qint64 inputFileLength = inputBuffer.size();
if (inputFileLength > BASE_STASH_HEADER_SIZE)
{
// offset 4 is almost always 0x01, except on corrupted character and stash files
if ((quint8)inputBuffer[4] == 0x01)
{
quint32 version = reinterpret_cast<const quint32*>(inputBuffer.data())[0];
bool isKnownSaveFileVersion = true;
qint64 inputOffset = 0;
qint64 outputFileLength = 0;
switch(version)
{
case 0x38:
{
qDebug() << "Beta version file, untested, your mileage may vary";
// for beta version we don't need to actually unscramble anything, just lop off the footer
// filesize of output will be the size of input minus four
outputFileLength = inputFileLength - STASH_FOOTER_SIZE;
inputOffset = BASE_STASH_HEADER_SIZE;
// isScrambled = false;
break;
}
case 0x40:
{
qDebug() << "Release version 1.9 file, scrambled, no checksum";
// filesize of output will be the size of input minus four
outputFileLength = inputFileLength - STASH_FOOTER_SIZE;
inputOffset = BASE_STASH_HEADER_SIZE;
break;
}
case 0x41:
case 0x42:
case 0x44:
{
qDebug() << "Patched version 1.10, 1.11 or 1.12 file, scrambled, with checksum";
// filesize of output will be the size of input minus eight
outputFileLength = inputFileLength - (STASH_CRC_SIZE + STASH_FOOTER_SIZE);
inputOffset = BASE_STASH_HEADER_SIZE + STASH_CRC_SIZE;
break;
}
default:
{
qDebug() << "Unknown savefile version!";
isKnownSaveFileVersion = false;
break;
}
}
if (isKnownSaveFileVersion)
{
qint64 outputOffset = BASE_STASH_HEADER_SIZE;
qint64 dataLength = inputFileLength - (inputOffset + STASH_FOOTER_SIZE);
outputBuffer.clear();
outputBuffer.fill(0, outputFileLength);
if (version >= 0x40)
{
for (int i = 0; i < BASE_STASH_HEADER_SIZE; ++i)
{
outputBuffer[i] = inputBuffer[i];
}
// descramble the data and place it in the correct place in the output buffer, after the header
Descramble(inputBuffer, inputOffset, dataLength, outputBuffer, outputOffset);
}
else
{
// just copy the data directly to the output buffer
outputBuffer.resize(dataLength + BASE_STASH_HEADER_SIZE);
outputBuffer.replace(0, dataLength + BASE_STASH_HEADER_SIZE, inputBuffer);
}
if (version >= 0x41)
{
qint32 claimedCRC = reinterpret_cast<const qint32*>(&(inputBuffer.data()[BASE_STASH_HEADER_SIZE]))[0];
qint32 calculatedCRC = CalculateChecksum(outputBuffer, outputOffset, dataLength);
qDebug() << "CRC of the data block: Header: " << claimedCRC << " Actual: " << calculatedCRC;
}
qint32 lengthClaimed = reinterpret_cast<const qint32*>(&(inputBuffer.data()[inputFileLength - 4]))[0];
qDebug() << "Claimed file length: " << lengthClaimed;
result = true;
}
}
}
return result;
}
bool Torchlight2StashConverter::ScrambleFile(const QByteArray& inputBuffer, QByteArray& outputBuffer)
{
bool result = false;
qint64 inputFileLength = inputBuffer.size();
if (inputFileLength > BASE_STASH_HEADER_SIZE)
{
// offset 4 is almost always 0x01, except on corrupted character and stash files
if ((quint8)inputBuffer[4] == 0x01)
{
quint32 version = reinterpret_cast<const quint32*>(inputBuffer.data())[0];
bool isKnownSaveFileVersion = true;
switch (version)
{
case 0x38:
case 0x40:
case 0x41:
{
version = 0x41;
break;
}
case 0x42:
case 0x44:
{
break;
}
default:
{
isKnownSaveFileVersion = false;
break;
}
}
if (isKnownSaveFileVersion)
{
qint32 outputFileLength = inputFileLength + STASH_CRC_SIZE + STASH_FOOTER_SIZE;
quint32 inputOffset = BASE_STASH_HEADER_SIZE;
quint32 outputOffset = BASE_STASH_HEADER_SIZE + STASH_CRC_SIZE;
quint32 dataLength = inputFileLength - BASE_STASH_HEADER_SIZE;
outputBuffer.clear();
outputBuffer.fill(0, outputFileLength);
for (int i = 0; i < 4; ++i)
{
outputBuffer[i] = inputBuffer[i];
}
outputBuffer[4] = 0x01;
quint32 crc = CalculateChecksum(inputBuffer, inputOffset, dataLength);
reinterpret_cast<quint32*>(&outputBuffer.data()[BASE_STASH_HEADER_SIZE])[0] = crc;
Scramble(inputBuffer, inputOffset, dataLength, outputBuffer, outputOffset);
reinterpret_cast<quint32*>(&(outputBuffer.data()[outputFileLength - 4]))[0] = outputFileLength;
result = true;
}
}
}
return result;
}