1#include "Game/DLC/aocManager.h"
2#include <container/seadBuffer.h>
3#include <filedevice/seadFileDeviceMgr.h>
4#include <prim/seadStringBuilder.h>
5#include <resource/seadSharcArchiveRes.h>
6#include "KingSystem/Resource/resLoadRequest.h"
7#include "KingSystem/Resource/resResourceMgrTask.h"
8#include "KingSystem/Utils/InitTimeInfo.h"
9#include "KingSystem/Utils/SafeDelete.h"
10
11#ifdef NNSDK
12#include <filedevice/nin/seadNinAocFileDeviceNin.h>
13#include <nn/aoc.h>
14#include <nn/fs.h>
15#include <prim/seadStringUtil.h>
16#endif
17
18namespace uking::aoc {
19
20namespace {
21
22struct DungeonInfo {
23 int number;
24 sead::SafeString flag;
25};
26
27ksys::util::InitConstants sInitConstants;
28DungeonInfo sDungeonInfoData[] = {
29 {120, "Defeat_OneHitDungeon001"},
30 {121, "BalladOfHeroGerudo_AppearDungeon03"},
31 {122, "Defeat_OneHitDungeon002"},
32 {123, "Defeat_OneHitDungeon003"},
33 {124, "BalladOfHeroZora_AppearDungeon01"},
34 {125, "BalladOfHeroZora_AppearDungeon02"},
35 {126, "BalladOfHeroZora_AppearDungeon03"},
36 {127, "BalladOfHeroRito_TargetHittingSuccess"},
37 {128, "BalladOfHeroRito_AppearDungeon03"},
38 {129, "BalladOfHeroRito_AppearDungeon02"},
39 {130, "BalladOfHeroGoron_FirstKillGolemR"},
40 {131, "BalladOfHeroGoron_AppearDungeon01"},
41 {132, "BalladOfHeroGoron_AppearDungeon03"},
42 {133, "BalladOfHeroGerudo_AppearDungeon02"},
43 {134, "BalladOfHeroGerudo_AppearDungeon01"},
44 {135, "Defeat_OneHitDungeon004"},
45};
46sead::Buffer<DungeonInfo> sDungeonInfo{sDungeonInfoData};
47constexpr int NumDungeons = 16;
48
49} // namespace
50
51SEAD_SINGLETON_DISPOSER_IMPL(Manager)
52
53Manager::Manager() : mVersionFile(), mGdtReinitSlot{this, &Manager::onGdtReinit} {
54 resetFlags();
55}
56
57Manager::~Manager() {
58 if (auto* gdm = ksys::gdt::Manager::instance())
59 gdm->removeReinitCallback(mGdtReinitSlot);
60
61 mAocMainFieldPackPrefix.deregister();
62 mAocPackPrefix.deregister();
63 mVersionFileDevPrefix.deregister();
64
65 if (mFileDevice) {
66 sead::FileDeviceMgr::instance()->unmount("aoc");
67 ksys::util::safeDelete(mFileDevice);
68 }
69
70#ifdef NNSDK
71 if (mAocFsCache) {
72 nn::fs::Unmount("aoc");
73 ksys::util::safeDelete(mAocFsCache);
74 }
75#endif
76}
77
78void Manager::resetFlags() {
79 mFlagAocVerAtLastPlay = ksys::gdt::InvalidHandle;
80 mFlagLatestAocVerPlayed = ksys::gdt::InvalidHandle;
81 mFlagHasAocVer1 = ksys::gdt::InvalidHandle;
82 mFlagHasAocVer2 = ksys::gdt::InvalidHandle;
83 mFlagHasAocVer3 = ksys::gdt::InvalidHandle;
84}
85
86void Manager::init(sead::Heap* heap) {
87#ifdef NNSDK
88 std::array<int, 2> dlc_info;
89 if (nn::aoc::ListAddOnContent(dlc_info.data(), 0, dlc_info.size()) == dlc_info.size() &&
90 (dlc_info[0] == 1 || dlc_info[1] == 1)) {
91 size_t cache_size = 0;
92 nn::fs::QueryMountAddOnContentCacheSize(&cache_size, 1);
93 const size_t cache_size_ = cache_size;
94 mAocFsCache = static_cast<u8*>(heap->tryAlloc(cache_size_, sizeof(void*)));
95
96 nn::fs::MountAddOnContent("aoc", 1, mAocFsCache, cache_size);
97
98 mFileDevice = new (heap) sead::NinAocFileDevice("aoc");
99 sead::FileDeviceMgr::instance()->mount(mFileDevice, "aoc");
100 loadVersionFile();
101 }
102#endif
103}
104
105void Manager::loadVersionFile() {
106 if (!mFileDevice)
107 return;
108
109 mVersionFileDevPrefix.registerPrefix("Aoc/0010/", mFileDevice, false);
110
111 ksys::res::LoadRequest req;
112 req.mRequester = "aocManager";
113 req._26 = false;
114 req.mAocFileDevice = mFileDevice;
115 const sead::SafeString path = "System/AocVersion.txt";
116 mVersionFile.file_handle.requestLoad(path, &req);
117}
118
119void Manager::loadAocMainFieldPack(ksys::OverlayArena* arena) {
120 if (!hasAoc3())
121 return;
122
123 auto* device = mFileDevice;
124 if (!device)
125 return;
126
127 ksys::res::LoadRequest req;
128 req.mRequester = "aocManager";
129 req._8 = true;
130 req._26 = false;
131 req.mAocFileDevice = device;
132 req.mArena = arena;
133 mAocMainFieldPack.requestLoad("Pack/AocMainField.pack", &req);
134}
135
136void Manager::registerAocMainFieldPack() {
137 mAocMainFieldPack.waitForReady();
138 mAocMainFieldPack.parseResource(nullptr);
139 if (mAocMainFieldPack.isSuccess())
140 mAocMainFieldPackPrefix.registerPrefix("Aoc/0010/", mAocMainFieldPack.getResource(), false);
141}
142
143void Manager::unloadAocMainFieldPack() {
144 mAocMainFieldPackPrefix.deregister();
145 mAocMainFieldPack.requestUnload();
146}
147
148sead::FileDevice* Manager::getFileDeviceForMapFile(const sead::SafeString& path) const {
149 if (!hasAoc2())
150 return nullptr;
151
152 auto* device = mFileDevice;
153
154 if (aocPackHasFile(path))
155 return nullptr;
156
157 if (path.startsWith("Map/MainField/"))
158 return device;
159
160 if (hasAoc3() && path.startsWith("Map/MainFieldDungeon/"))
161 return device;
162
163 if (isAocFile(path, "Map/", "Map/AocField/", "Map/MainFieldDungeon/", "Map/CDungeon/"))
164 return device;
165
166 return nullptr;
167}
168
169bool Manager::getFileDeviceForMap(sead::FileDevice** p_file_device, ksys::res::Handle** p_handle,
170 const sead::SafeString& path) {
171 if (!hasAoc2())
172 return false;
173
174 auto* device = mFileDevice;
175
176 if (path.startsWith("Map/MainField/")) {
177 if (mAocMainFieldPack.isSuccess())
178 *p_handle = &mAocMainFieldPack;
179 else
180 *p_file_device = device;
181 return true;
182 }
183
184 if (aocPackHasFile(path))
185 return false;
186
187 if (hasAoc3() && path.startsWith("Map/MainFieldDungeon/")) {
188 *p_file_device = device;
189 return true;
190 }
191
192 if (isAocFile(path, "Map/", "Map/AocField/", "Map/MainFieldDungeon/", "Map/CDungeon/")) {
193 *p_file_device = device;
194 return true;
195 }
196
197 return false;
198}
199
200sead::FileDevice* Manager::getFileDeviceForStaticCompound(const sead::SafeString& path) const {
201 if (!hasAoc2())
202 return nullptr;
203
204 auto* device = mFileDevice;
205
206 if (aocPackHasFile(path))
207 return nullptr;
208
209 if (isAocFile(path, "Physics/StaticCompound/", "Physics/StaticCompound/AocField/",
210 "Physics/StaticCompound/MainFieldDungeon/", "Physics/StaticCompound/CDungeon/")) {
211 return device;
212 }
213
214 return nullptr;
215}
216
217sead::FileDevice* Manager::getFileDeviceForTeraMesh(const sead::SafeString& path) const {
218 if (!hasAoc2())
219 return nullptr;
220
221 if (path.startsWith("Physics/TeraMeshRigidBody/AocField/"))
222 return mFileDevice;
223
224 return nullptr;
225}
226
227sead::FileDevice* Manager::getFileDeviceForNavMesh(const sead::SafeString& path) const {
228 if (!hasAoc2())
229 return nullptr;
230
231 auto* device = mFileDevice;
232
233 if (aocPackHasFile(path))
234 return nullptr;
235
236 if (isAocFile(path, "NavMesh/", "NavMesh/AocField/", "NavMesh/MainFieldDungeon/",
237 "NavMesh/CDungeon/")) {
238 return device;
239 }
240
241 return nullptr;
242}
243
244sead::FileDevice* Manager::getFileDeviceForTerrain(const sead::SafeString& path) const {
245 if (!hasAoc2())
246 return nullptr;
247
248 if (path.startsWith("Terrain/A/AocField"))
249 return mFileDevice;
250
251 return nullptr;
252}
253
254sead::FileDevice* Manager::getFileDeviceForGame(const sead::SafeString& path) const {
255 if (!hasAoc2())
256 return nullptr;
257
258 if (path.startsWith("Game/AocField/"))
259 return mFileDevice;
260
261 return nullptr;
262}
263
264sead::FileDevice* Manager::getFileDeviceForUI(const sead::SafeString& path) const {
265 if (!hasAoc3())
266 return nullptr;
267
268 if (path.startsWith("UI/StaffRollDLC/"))
269 return mFileDevice;
270
271 return nullptr;
272}
273
274bool Manager::isAocFile(const sead::SafeString& path, const sead::SafeString& dir,
275 const sead::SafeString& dir_aoc_field,
276 const sead::SafeString& dir_main_field_dungeon,
277 const sead::SafeString& dir_cdungeon) const {
278 if (!path.startsWith(dir))
279 return false;
280
281 if (path.startsWith(dir_aoc_field))
282 return true;
283
284 if (path.startsWith(dir_main_field_dungeon)) {
285 const auto rel_path = path.getPart(dir_main_field_dungeon.calcLength());
286 if (rel_path.startsWith("RemainsElectric"))
287 return false;
288 if (rel_path.startsWith("RemainsFire"))
289 return false;
290 if (rel_path.startsWith("RemainsWater"))
291 return false;
292 if (rel_path.startsWith("RemainsWind"))
293 return false;
294 return true;
295 }
296
297 if (path.startsWith(dir_cdungeon)) {
298 const auto rel_path = path.getPart(dir_cdungeon.calcLength());
299 return isAocDungeon(rel_path);
300 }
301
302 return false;
303}
304
305static bool isAocDungeonNumber(const sead::SafeString& number) {
306 int num;
307 sead::FixedSafeString<4> buffer;
308 buffer.copy(number, 3);
309 return sead::StringUtil::tryParseS32(&num, buffer, sead::StringUtil::CardinalNumber::Base10) &&
310 num >= 120;
311}
312
313bool Manager::isAocDungeon(const sead::SafeString& map_name) {
314 if (!map_name.startsWith("Dungeon"))
315 return true;
316 const auto number = map_name.getPart(sead::SafeString("Dungeon").calcLength());
317 return isAocDungeonNumber(number);
318}
319
320bool Manager::isAocMap(const sead::SafeString& map_type, const sead::SafeString& map_name) {
321 if (isAocField(map_type))
322 return true;
323
324 if (map_type == "MainFieldDungeon") {
325 if (map_name == "RemainsElectric")
326 return false;
327 if (map_name == "RemainsFire")
328 return false;
329 if (map_name == "RemainsWater")
330 return false;
331 if (map_name == "RemainsWind")
332 return false;
333 return true;
334 }
335
336 if (map_type == "CDungeon")
337 return isAocDungeon(map_name);
338
339 return false;
340}
341
342bool Manager::isAocField(const sead::SafeString& map_type) {
343 return map_type == "AocField";
344}
345
346sead::FileDevice* Manager::getFileDeviceForDungeonPack(const sead::SafeString& path) const {
347 if (!hasAoc3())
348 return nullptr;
349
350 auto* device = mFileDevice;
351
352 const sead::SafeString prefix = "Pack/";
353 const auto prefix_len = prefix.calcLength();
354
355 if (!path.startsWith(prefix))
356 return nullptr;
357
358 const auto rel_path = path.getPart(prefix_len);
359
360 if (rel_path.startsWith("Dungeon") && !isAocDungeon(rel_path))
361 device = nullptr;
362
363 return device;
364}
365
366void Manager::registerAocPack(ksys::res::Handle* pack) {
367 mAocPack = pack;
368 if (pack) {
369 mAocPackPrefix.registerPrefix("Aoc/0010/", pack->getResource(), true);
370 } else {
371 mAocPackPrefix.deregister();
372 }
373}
374
375bool Manager::aocPackHasFile(const sead::SafeString& path) const {
376 if (!mAocPack)
377 return false;
378
379 const auto* res = sead::DynamicCast<sead::SharcArchiveRes>(mAocPack->getResource());
380 if (!res)
381 return false;
382
383 sead::FixedStringBuilder<0x81> builder;
384 builder.copy(path.cstr());
385 ksys::res::ResourceMgrTask::instance()->addSExtensionPrefix(builder);
386 return res->isExistFile(builder);
387}
388
389bool Manager::changeMoviePath(sead::BufferedSafeString& path) const {
390 if (!hasAoc3())
391 return false;
392
393 const sead::SafeString prefix = "Movie/";
394 const auto prefix_len = prefix.calcLength();
395 if (!path.startsWith(prefix))
396 return false;
397
398 const auto rel_path = path.getPart(prefix_len);
399 if (!rel_path.startsWith("Demo6"))
400 return false;
401
402 path.prepend("aoc://");
403 return true;
404}
405
406bool Manager::parseVersion() {
407 if (!mVersionFile.readVersion())
408 return false;
409
410 if (mVersionFile.string.isEmpty()) {
411 mVersion = 0;
412 return true;
413 }
414
415 {
416 const int dot_index = mVersionFile.string.findIndex(".");
417 const int minor_index = dot_index + 1;
418 if (dot_index <= 0 || minor_index >= mVersionFile.string.calcLength()) {
419 mVersionFile.string.clear();
420 mVersion = 0;
421 return true;
422 }
423
424 u32 major, minor;
425 const auto parse = [&] {
426 sead::FixedSafeString<16> major_str;
427 major_str.copy(mVersionFile.string, dot_index);
428
429 constexpr auto base = sead::StringUtil::CardinalNumber::Base10;
430 if (!sead::StringUtil::tryParseU32(&major, major_str, base))
431 return false;
432
433 const auto minor_str = mVersionFile.string.getPart(minor_index);
434 if (!sead::StringUtil::tryParseU32(&minor, minor_str, base))
435 return false;
436
437 return true;
438 };
439
440 if (!parse()) {
441 mVersionFile.string.clear();
442 mVersion = 0;
443 return true;
444 }
445
446 mVersion = (major << 8) + minor;
447 }
448
449 checkVersion();
450 return true;
451}
452
453// NON_MATCHING: stack and duplicated branch -- volatile variables are painful
454void Manager::checkVersion() {
455 if (mVersion == 0)
456 return;
457
458 const auto fail = [this](VersionError error) {
459 mVersionFlags.setBit(error);
460 mVersion = 0;
461 };
462
463 if (mVersion < 0x300) {
464 fail(VersionError::TooOld);
465 } else if (mVersion >= 0x400) {
466 fail(VersionError::TooNew);
467 }
468
469 mVersionFlags.isOnBit(VersionError(VersionError::TooOld));
470 mVersionFlags.isOnBit(VersionError(VersionError::TooNew));
471}
472
473bool Manager::VersionFile::readVersion() {
474 if (!file_handle.requestedLoad())
475 return true;
476
477 if (file_handle.isSuccess()) {
478 auto* res = sead::DynamicCast<sead::DirectResource>(file_handle.getResource());
479 string.copy(reinterpret_cast<const char*>(res->getRawData()), res->getRawSize());
480 } else {
481 if (!file_handle.checkLoadStatus())
482 return false;
483 string.clear();
484 }
485
486 file_handle.requestUnload();
487 return true;
488}
489
490void Manager::initGameData() {
491 reinitFlags();
492 ksys::gdt::Manager::instance()->addReinitCallback(mGdtReinitSlot);
493}
494
495static const sead::SafeString& getDungeonFlag(int number) {
496 for (auto it = sDungeonInfo.begin(); it != sDungeonInfo.end(); ++it) {
497 if (it->number == number)
498 return it->flag;
499 }
500 return sead::SafeString::cEmptyString;
501}
502
503void Manager::reinitFlags() {
504 mFlagAocVerAtLastPlay = ksys::gdt::Manager::instance()->getS32Handle("AoCVerAtLastPlay");
505 mFlagLatestAocVerPlayed = ksys::gdt::Manager::instance()->getS32Handle("LatestAoCVerPlayed");
506 mFlagHasAocVer1 =
507 ksys::gdt::Manager::instance()->getBoolHandle(GameDataFlag::text(GameDataFlag::HasAoCVer1));
508 mFlagHasAocVer2 =
509 ksys::gdt::Manager::instance()->getBoolHandle(GameDataFlag::text(GameDataFlag::HasAoCVer2));
510 mFlagHasAocVer3 =
511 ksys::gdt::Manager::instance()->getBoolHandle(GameDataFlag::text(GameDataFlag::HasAoCVer3));
512
513 for (int i = 0; i < mDLCPositions.size(); ++i) {
514 ksys::gdt::FlagHandle handle = ksys::gdt::InvalidHandle;
515
516 if (i < NumDungeons) {
517 if (mDLCPositions[i].flag_handle == ksys::gdt::InvalidHandle)
518 continue;
519
520 handle = ksys::gdt::Manager::instance()->getBoolHandle(getDungeonFlag(i + 120));
521 }
522
523 mDLCPositions[i].flag_handle = handle;
524 }
525}
526
527void Manager::setGameDataFlags() const {
528 ksys::gdt::Manager::instance()->setS32(mVersion, mFlagAocVerAtLastPlay);
529
530 s32 latest_ver;
531 if (ksys::gdt::Manager::instance()->getS32(mFlagLatestAocVerPlayed, &latest_ver)) {
532 if (u32(latest_ver) < mVersion)
533 ksys::gdt::Manager::instance()->setS32(mVersion, mFlagLatestAocVerPlayed);
534 }
535
536 const auto ver = mVersion;
537 ksys::gdt::Manager::instance()->setBool(ver >= 0x100, mFlagHasAocVer1);
538 ksys::gdt::Manager::instance()->setBool(ver >= 0x200, mFlagHasAocVer2);
539 ksys::gdt::Manager::instance()->setBool(ver >= 0x300, mFlagHasAocVer3);
540}
541
542void Manager::onGdtReinit(ksys::gdt::Manager::ReinitEvent* event) {
543 reinitFlags();
544}
545
546} // namespace uking::aoc
547