-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathrandomizer.cpp
368 lines (310 loc) · 13.8 KB
/
randomizer.cpp
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
356
357
358
359
360
361
362
363
364
365
366
367
368
#include "randomizer.hpp"
#include <string>
#include <vector>
#include <filesystem>
#include <libs/tinyxml2.hpp>
#include <libs/zlib-ng.hpp>
#include <tweaks.hpp>
#include <seedgen/config.hpp>
#include <seedgen/random.hpp>
#include <seedgen/seed.hpp>
#include <logic/SpoilerLog.hpp>
#include <logic/Generate.hpp>
#include <logic/LogicTests.hpp>
#include <command/WriteLocations.hpp>
#include <command/WriteEntrances.hpp>
#include <command/WriteCharts.hpp>
#include <command/RandoSession.hpp>
#include <command/Log.hpp>
#include <utility/platform.hpp>
#include <utility/path.hpp>
#include <utility/file.hpp>
#include <utility/time.hpp>
#include <gui/desktop/update_dialog_header.hpp>
#ifdef DEVKITPRO
#include <sysapp/title.h>
#include <platform/channel.hpp>
#include <gui/wiiu/InstallMenu.hpp>
#endif
class Randomizer {
private:
Config config;
#ifdef DRY_RUN
const bool dryRun = true;
#else
const bool dryRun = false;
#endif
//bool randomizeItems = true; not currently used
unsigned int numPlayers = 1;
//int playerId = 1;
[[nodiscard]] bool verifyBase() {
using namespace std::filesystem;
Utility::platformLog("Verifying dump...");
UPDATE_DIALOG_LABEL("Verifying dump...");
UPDATE_DIALOG_VALUE(25);
const fspath& base = g_session.getBaseDir();
if(!is_directory(base / "code") || !is_directory(base / "content") || !is_directory(base / "meta")) {
ErrorLog::getInstance().log("Invalid base path: could not find code/content/meta folders at " + Utility::toUtf8String(base) + "!");
return false;
}
//Check the meta.xml for other platforms (+ a sanity check on console)
const fspath& metaPath = g_session.getBaseDir() / "meta/meta.xml";
if(!is_regular_file(metaPath)) {
ErrorLog::getInstance().log("Failed finding meta.xml");
return false;
}
tinyxml2::XMLDocument metaXml;
if(const tinyxml2::XMLError err = LoadXML(metaXml, metaPath); err != tinyxml2::XMLError::XML_SUCCESS) {
ErrorLog::getInstance().log(std::string("Could not parse input's meta.xml, ") + metaXml.ErrorStr());
return false;
}
const tinyxml2::XMLElement* root = metaXml.RootElement();
const std::string titleId = root->FirstChildElement("title_id")->GetText();
const std::string nameEn = root->FirstChildElement("longname_en")->GetText();
if(titleId != "0005000010143500" || nameEn != "THE LEGEND OF ZELDA\nThe Wind Waker HD") {
if(titleId == "0005000010143400" || titleId == "0005000010143600") {
ErrorLog::getInstance().log("Incorrect region - game must be a NTSC-U / US copy");
}
else {
ErrorLog::getInstance().log("meta.xml does not match base game - dump is not valid");
ErrorLog::getInstance().log("ID " + titleId);
ErrorLog::getInstance().log("Name " + nameEn);
}
return false;
}
const std::string region = root->FirstChildElement("region")->GetText();
if(region != "00000002") {
ErrorLog::getInstance().log("Incorrect region - game must be a NTSC-U / US copy");
return false;
}
return true;
}
[[nodiscard]] bool verifyOutput() {
using namespace std::filesystem;
Utility::platformLog("Verifying output...");
UPDATE_DIALOG_LABEL("Verifying output...");
UPDATE_DIALOG_VALUE(25);
const fspath& out = g_session.getOutputDir();
if(out.empty() || !is_directory(out)) { // we tried to create this earlier so error if that failed
ErrorLog::getInstance().log("Invalid output path: " + Utility::toUtf8String(out) + " is not a valid folder!");
return false;
}
if(!is_directory(out / "code") || !is_directory(out / "content") || !is_directory(out / "meta")) {
#ifdef DEVKITPRO // this should all exist on console
ErrorLog::getInstance().log("Invalid output path: could not find code/content/meta folders at " + out.string() + "!");
return false;
#else // copy over the files on desktop
g_session.setFirstTimeSetup(true);
return true; // skip the rest of the checks, don't error
#endif
}
//Double check the meta.xml
const fspath& metaPath = out / "meta/meta.xml";
if(!is_regular_file(metaPath)) {
ErrorLog::getInstance().log("Failed finding meta.xml");
return false;
}
tinyxml2::XMLDocument metaXml;
if(const tinyxml2::XMLError err = LoadXML(metaXml, metaPath); err != tinyxml2::XMLError::XML_SUCCESS) {
ErrorLog::getInstance().log(std::string("Could not parse output's meta.xml, ") + metaXml.ErrorStr());
return false;
}
const tinyxml2::XMLElement* root = metaXml.RootElement();
// Title ID won't be updated until after the first randomization on PC so it's not a very useful check
// But on console it should be correct once the channel is installed so it's a good sanity check
#ifdef DEVKITPRO
const std::string titleId = root->FirstChildElement("title_id")->GetText();
if(titleId != "0005000010143599") {
ErrorLog::getInstance().log("meta.xml does not match - custom channel is not valid");
ErrorLog::getInstance().log("ID " + titleId);
return false;
}
#endif
const std::string region = root->FirstChildElement("region")->GetText();
if(region != "00000002") {
ErrorLog::getInstance().log("Incorrect region - game must be a NTSC-U / US copy");
return false;
}
return true;
}
public:
Randomizer(const Config& config_) :
config(config_)
{}
int randomize() {
// Go through the setting testing process if mass testing is turned on and ignore everything else
#ifdef LOGIC_TESTS
#if TEST_COUNT
testSettings(config, TEST_COUNT);
#else
runLogicTests(config);
#endif
return 0;
#endif
// Only set up the session if we actually need it
if(!dryRun) {
if(!g_session.init(config.gameBaseDir, config.outputDir)) {
ErrorLog::getInstance().log("Failed to initialize session");
return 1;
}
Utility::platformLog("Initialized session");
}
LogInfo::setConfig(config);
LOG_TO_DEBUG("Permalink: " + config.getPermalink());
// Seed RNG
const std::string permalink = config.getPermalink(true);
if(permalink.empty()) {
ErrorLog::getInstance().log("Could not generate permalink for RNG seeding.");
return 1;
}
const size_t integer_seed = zng_crc32(0L, reinterpret_cast<const uint8_t*>(permalink.data()), permalink.length());
Random_Init(integer_seed);
LogInfo::setSeedHash(generate_seed_hash());
UPDATE_DIALOG_TITLE("Randomizing - Hash: " + LogInfo::getSeedHash());
// Create all necessary worlds (for any potential multiworld support in the future)
WorldPool worlds(numPlayers);
std::vector<Settings> settingsVector (numPlayers, config.settings);
Utility::platformLog("Randomizing...");
UPDATE_DIALOG_VALUE(5);
if (generateWorlds(worlds, settingsVector) != 0) {
return 1;
}
generateNonSpoilerLog(worlds);
if (!config.settings.do_not_generate_spoiler_log) {
generateSpoilerLog(worlds);
}
// Skip all game modification stuff if we're doing a dry run (fill testing)
if (dryRun) return 0;
if(!verifyBase()) {
return 1;
}
if(!verifyOutput()) {
return 1;
}
//IMPROVEMENT: custom model things
if(const ModelError err = config.settings.selectedModel.applyModel(); err != ModelError::NONE) {
ErrorLog::getInstance().log("Failed to apply custom model, error " + errorToName(err));
return 1;
}
Utility::platformLog("Modifying game code...");
UPDATE_DIALOG_VALUE(30);
UPDATE_DIALOG_LABEL("Modifying game code...");
// TODO: update worlds indexing for multiworld eventually
if(const TweakError err = apply_necessary_tweaks(worlds[0].getSettings()); err != TweakError::NONE) {
ErrorLog::getInstance().log("Encountered " + errorGetName(err) + " in pre-randomization tweaks!");
return 1;
}
// Assume 1 world for now, modifying multiple copies needs work
// These work directly on the data stream so RandoSession applies them first
if(!writeLocations(worlds)) {
ErrorLog::getInstance().log("Failed to save items!");
return 1;
}
// Charts/entrances work through the actor list, which is applied after any stream modifications
// This prevents them from interfering with the hardcoded item offsets
if(config.settings.randomize_charts) {
if(!writeCharts(worlds)) {
ErrorLog::getInstance().log("Failed to save charts!");
return 1;
}
}
if (config.settings.randomize_dungeon_entrances ||
config.settings.randomize_boss_entrances ||
config.settings.randomize_miniboss_entrances ||
config.settings.randomize_cave_entrances != ShuffleCaveEntrances::Disabled ||
config.settings.randomize_door_entrances ||
config.settings.randomize_misc_entrances) {
if(!writeEntrances(worlds)) {
ErrorLog::getInstance().log("Failed to save entrances!");
return 1;
}
}
Utility::platformLog("Applying final patches...");
UPDATE_DIALOG_VALUE(50);
UPDATE_DIALOG_LABEL("Applying final patches...");
if(const TweakError err = apply_necessary_post_randomization_tweaks(worlds[0]/* , randomizeItems */); err != TweakError::NONE) {
ErrorLog::getInstance().log("Encountered " + errorGetName(err) + " in post-randomization tweaks!");
return 1;
}
// Restore files that aren't changed (chart list, entrances, etc) so they don't persist across seeds
// restoreGameFile() does not need to check if the file is cached because it only tries to get the cache entry
// Getting a cache entry doesn't overwrite anything if the file already had modifications
Utility::platformLog("Restoring outdated files...");
if(!g_session.restoreGameFile("content/Common/Misc/Misc.szs")) {
ErrorLog::getInstance().log("Failed to restore Misc.szs!");
return 1;
}
if(!g_session.restoreGameFile("content/Common/Particle/Particle.szs")) {
ErrorLog::getInstance().log("Failed to restore Particle.szs!");
return 1;
}
if(!g_session.restoreGameFile("content/Common/Pack/permanent_3d.pack")) {
ErrorLog::getInstance().log("Failed to restore permanent_3d.pack!");
return 1;
}
if(!restoreEntrances(worlds)) {
ErrorLog::getInstance().log("Failed to restore entrances!");
return 1;
}
Utility::platformLog("Preparing to edit files...");
if(!g_session.modFiles()) {
ErrorLog::getInstance().log("Failed to edit file cache!");
return 1;
}
//done!
return 0;
}
};
int mainRandomize() {
#ifdef ENABLE_TIMING
ScopedTimer<"Total process took "> timer;
#endif
// Make sure we have a logs folder
if (!Utility::create_directories(Utility::get_logs_path())) {
ErrorLog::getInstance().log("Failed to create logs folder");
return 1;
}
// Create default configs/preferences if they don't exist
ConfigError err = Config::writeDefault(Utility::get_app_save_path() / "config.yaml", Utility::get_app_save_path() / "preferences.yaml");
if(err != ConfigError::NONE) {
ErrorLog::getInstance().log("Failed to create config, error " + ConfigErrorGetName(err));
return 1;
}
Utility::platformLog("Reading config");
Config load;
err = load.loadFromFile(Utility::get_app_save_path() / "config.yaml", Utility::get_app_save_path() / "preferences.yaml");
if(err != ConfigError::NONE && err != ConfigError::DIFFERENT_RANDO_VERSION) {
ErrorLog::getInstance().log("Failed to read config, error " + ConfigErrorGetName(err));
return 1;
}
#ifdef DEVKITPRO
if(!SYSCheckTitleExists(0x0005000010143500)) {
ErrorLog::getInstance().log("Could not find game: you must have a NTSC-U / US copy of TWWHD!");
return 1;
}
if(const auto& err = getTitlePath(0x0005000010143500, load.gameBaseDir); err < 0) {
return 1;
}
if(!Utility::mountDeviceAndConvertPath(load.gameBaseDir)) {
ErrorLog::getInstance().log("Failed mounting input device!");
return 1;
}
if(!SYSCheckTitleExists(0x0005000010143599)) {
if(!createOutputChannel(load.gameBaseDir, pickInstallLocation())) {
return 1;
}
g_session.setFirstTimeSetup(true);
}
if(const auto& err = getTitlePath(0x0005000010143599, load.outputDir); err < 0) {
return 1;
}
if(!Utility::mountDeviceAndConvertPath(load.outputDir)) {
ErrorLog::getInstance().log("Failed mounting output device!");
return 1;
}
#endif
Randomizer rando(load);
// IMPROVEMENT: issue with seekp, find better solution than manual padding?
// TODO: make things zoom
return rando.randomize();
}