-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinjector.cpp
More file actions
355 lines (296 loc) · 11.3 KB
/
Copy pathinjector.cpp
File metadata and controls
355 lines (296 loc) · 11.3 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
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
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
#include "injector.h"
#include <fstream>
#include <sys/stat.h>
#include <cstring>
namespace Injector {
// Version suffixes to try, in order (newest first)
static const char* VERSION_SUFFIXES[] = { "_3_0_0", "_2_0_0", "_1_3_0", "" };
static constexpr int NUM_SUFFIXES = 4;
static bool fileExists(const std::string& path) {
struct stat st;
return stat(path.c_str(), &st) == 0 && S_ISREG(st.st_mode);
}
// Try to find a file with version fallback. Returns full path or empty string.
static std::string findVersionedFile(const std::string& dir, const char* baseName) {
for (int i = 0; i < NUM_SUFFIXES; i++) {
std::string path = dir + baseName + VERSION_SUFFIXES[i];
if (fileExists(path))
return path;
}
return "";
}
static std::vector<uint8_t> readBinaryFile(const std::string& path) {
std::ifstream file(path, std::ios::binary | std::ios::ate);
if (!file.is_open())
return {};
auto size = file.tellg();
file.seekg(0);
std::vector<uint8_t> data(size);
file.read(reinterpret_cast<char*>(data.data()), size);
return data;
}
static std::string readTextFile(const std::string& path) {
std::ifstream file(path);
if (!file.is_open())
return "";
std::string content;
std::getline(file, content);
// Trim trailing whitespace/newlines
while (!content.empty() && (content.back() == '\r' || content.back() == '\n' || content.back() == ' '))
content.pop_back();
return content;
}
bool isValidOutbreakFolder(const std::string& path) {
if (!fileExists(path + "Identifier.txt"))
return false;
std::string filesDir = path + "Files/";
// Required: pokedata + Paldea + Kitakami zones (Outbreaks BCAT shipped in 2.0.0)
if (findVersionedFile(filesDir, "pokedata_array").empty())
return false;
if (findVersionedFile(filesDir, "zone_main_array").empty())
return false;
if (findVersionedFile(filesDir, "zone_su1_array").empty())
return false;
// zone_su2_array (Blueberry) is optional, only present in 3.0.0+ events.
return true;
}
bool isValidRaidFolder(const std::string& path) {
// Check Identifier.txt
if (!fileExists(path + "Identifier.txt"))
return false;
std::string filesDir = path + "Files/";
// Check all 5 required binary files (with version fallback)
if (findVersionedFile(filesDir, "event_raid_identifier").empty())
return false;
if (findVersionedFile(filesDir, "raid_enemy_array").empty())
return false;
if (findVersionedFile(filesDir, "fixed_reward_item_array").empty())
return false;
if (findVersionedFile(filesDir, "lottery_reward_item_array").empty())
return false;
if (findVersionedFile(filesDir, "raid_priority_array").empty())
return false;
return true;
}
InjectorResult injectRaidEvent(SaveFile& save, const std::string& folderPath) {
InjectorResult result{};
if (!save.isLoaded()) {
result.message = "Save file not loaded";
return result;
}
// Ensure trailing slash
std::string path = folderPath;
if (!path.empty() && path.back() != '/')
path += '/';
// Read identifier
result.identifier = readTextFile(path + "Identifier.txt");
if (result.identifier.empty()) {
result.message = "Failed to read Identifier.txt";
return result;
}
std::string filesDir = path + "Files/";
// Find and read all binary files with version fallback
struct FileSpec {
const char* baseName;
uint32_t blockKey;
};
static const FileSpec specs[] = {
{ "event_raid_identifier", KEY_RAID_IDENTIFIER },
{ "raid_enemy_array", KEY_RAID_ENEMY_ARRAY },
{ "fixed_reward_item_array", KEY_FIXED_REWARD_ARRAY },
{ "lottery_reward_item_array", KEY_LOTTERY_REWARD_ARRAY },
{ "raid_priority_array", KEY_RAID_PRIORITY_ARRAY },
};
for (const auto& spec : specs) {
std::string filePath = findVersionedFile(filesDir, spec.baseName);
if (filePath.empty()) {
result.message = std::string("Missing file: ") + spec.baseName;
return result;
}
std::vector<uint8_t> data = readBinaryFile(filePath);
if (data.empty()) {
result.message = std::string("Failed to read: ") + spec.baseName;
return result;
}
if (!save.replaceBlockData(spec.blockKey, data)) {
result.message = std::string("Failed to inject block: ") + spec.baseName;
return result;
}
}
result.success = true;
result.message = "Successfully imported event [" + result.identifier + "]";
return result;
}
InjectorResult injectOutbreakEvent(SaveFile& save, const std::string& folderPath) {
InjectorResult result{};
if (!save.isLoaded()) {
result.message = "Save file not loaded";
return result;
}
std::string path = folderPath;
if (!path.empty() && path.back() != '/')
path += '/';
result.identifier = readTextFile(path + "Identifier.txt");
if (result.identifier.empty()) {
result.message = "Failed to read Identifier.txt";
return result;
}
std::string filesDir = path + "Files/";
// Required blocks: pokedata, Paldea zones, Kitakami zones.
struct FileSpec {
const char* baseName;
uint32_t blockKey;
bool required;
};
static const FileSpec specs[] = {
{ "pokedata_array", KEY_OUTBREAK_POKEDATA, true },
{ "zone_main_array", KEY_OUTBREAK_ZONES_PALDEA, true },
{ "zone_su1_array", KEY_OUTBREAK_ZONES_KITAKAMI, true },
{ "zone_su2_array", KEY_OUTBREAK_ZONES_BLUEBERRY, false },
};
for (const auto& spec : specs) {
std::string filePath = findVersionedFile(filesDir, spec.baseName);
if (filePath.empty()) {
if (!spec.required)
continue;
result.message = std::string("Missing file: ") + spec.baseName;
return result;
}
std::vector<uint8_t> data = readBinaryFile(filePath);
if (data.empty()) {
result.message = std::string("Failed to read: ") + spec.baseName;
return result;
}
// Match Tera-Finder FinalizeImportOutbreak: skip blocks whose Type is None
// (or absent). Creating new blocks would change the encrypted file size,
// which is unsafe on the Switch journaled save filesystem.
SCBlock* block = save.findBlock(spec.blockKey);
if (!block || block->type == SCTypeCode::None)
continue;
if (!save.replaceBlockData(spec.blockKey, data)) {
result.message = std::string("Failed to inject block: ") + spec.baseName;
return result;
}
}
// Enable the BCAT outbreak event flag. Tera-Finder only flips it if it
// currently exists with a non-None type — preserve that behavior.
SCBlock* enabled = save.findBlock(KEY_OUTBREAK_ENABLED);
if (enabled && enabled->type != SCTypeCode::None) {
if (!save.setBoolBlock(KEY_OUTBREAK_ENABLED, true)) {
result.message = "Failed to enable outbreak event flag";
return result;
}
}
result.success = true;
result.message = "Successfully imported outbreak event [" + result.identifier + "]";
return result;
}
InjectorResult injectNullEvent(SaveFile& save) {
InjectorResult result{};
if (!save.isLoaded()) {
result.message = "Save file not loaded";
return result;
}
struct NullSpec {
uint32_t key;
size_t size;
};
static const NullSpec specs[] = {
{ KEY_RAID_IDENTIFIER, SIZE_RAID_IDENTIFIER },
{ KEY_RAID_ENEMY_ARRAY, SIZE_RAID_ENEMY_ARRAY },
{ KEY_FIXED_REWARD_ARRAY, SIZE_FIXED_REWARD_ARRAY },
{ KEY_LOTTERY_REWARD_ARRAY, SIZE_LOTTERY_REWARD_ARRAY },
{ KEY_RAID_PRIORITY_ARRAY, SIZE_RAID_PRIORITY_ARRAY },
};
for (const auto& spec : specs) {
std::vector<uint8_t> nullData(spec.size, 0);
if (!save.replaceBlockData(spec.key, nullData)) {
result.message = "Failed to clear raid block";
return result;
}
}
result.success = true;
result.identifier = "Null";
result.message = "Successfully cleared raid event data";
return result;
}
InjectorResult injectNullOutbreakEvent(SaveFile& save) {
InjectorResult result{};
if (!save.isLoaded()) {
result.message = "Save file not loaded";
return result;
}
struct NullSpec {
uint32_t key;
size_t size;
};
static const NullSpec specs[] = {
{ KEY_OUTBREAK_POKEDATA, SIZE_OUTBREAK_POKEDATA },
{ KEY_OUTBREAK_ZONES_PALDEA, SIZE_OUTBREAK_ZONES_PALDEA },
{ KEY_OUTBREAK_ZONES_KITAKAMI, SIZE_OUTBREAK_ZONES_KITAKAMI },
{ KEY_OUTBREAK_ZONES_BLUEBERRY, SIZE_OUTBREAK_ZONES_BLUEBERRY },
};
for (const auto& spec : specs) {
SCBlock* block = save.findBlock(spec.key);
if (!block || block->type == SCTypeCode::None)
continue;
std::vector<uint8_t> nullData(spec.size, 0);
if (!save.replaceBlockData(spec.key, nullData)) {
result.message = "Failed to clear outbreak block";
return result;
}
}
// Match Tera-Finder's "000 Null Outbreak Event" behavior: BCAT stays
// enabled, payload is zeroed.
SCBlock* enabled = save.findBlock(KEY_OUTBREAK_ENABLED);
if (enabled && enabled->type != SCTypeCode::None) {
if (!save.setBoolBlock(KEY_OUTBREAK_ENABLED, true)) {
result.message = "Failed to enable outbreak event flag";
return result;
}
}
result.success = true;
result.identifier = "Null";
result.message = "Successfully cleared outbreak event data";
return result;
}
InjectorResult resetSevenStarCaptures(SaveFile& save) {
InjectorResult result{};
if (!save.isLoaded()) {
result.message = "Save file not loaded";
return result;
}
SCBlock* block = save.findBlock(KEY_SEVEN_STAR_CAPTURE);
if (!block || block->type == SCTypeCode::None || block->data.empty()) {
// No 7-star capture history in this save: nothing to do. Not an error.
result.success = true;
result.count = 0;
result.message = "No 7-Star raid captures to reset";
return result;
}
// Read-modify-write: clear the captured byte of each 8-byte record.
std::vector<uint8_t> data = block->data;
int cleared = 0;
for (size_t i = 0; i + SEVEN_STAR_RECORD_SIZE <= data.size(); i += SEVEN_STAR_RECORD_SIZE) {
uint8_t& captured = data[i + SEVEN_STAR_OFFSET_CAPTURED];
if (captured != 0) {
captured = 0; // leave the defeated flag (offset 0x05) untouched
++cleared;
}
}
if (cleared == 0) {
result.success = true;
result.count = 0;
result.message = "No captured 7-Star raids found to reset";
return result;
}
if (!save.replaceBlockData(KEY_SEVEN_STAR_CAPTURE, data)) {
result.message = "Failed to write 7-Star raid capture block";
return result;
}
result.success = true;
result.count = cleared;
result.message = "Reset " + std::to_string(cleared) + " 7-Star raid capture(s)";
return result;
}
} // namespace Injector