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 | |
18 | namespace uking::aoc { |
19 | |
20 | namespace { |
21 | |
22 | struct DungeonInfo { |
23 | int number; |
24 | sead::SafeString flag; |
25 | }; |
26 | |
27 | ksys::util::InitConstants sInitConstants; |
28 | DungeonInfo 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 | }; |
46 | sead::Buffer<DungeonInfo> sDungeonInfo{sDungeonInfoData}; |
47 | constexpr int NumDungeons = 16; |
48 | |
49 | } // namespace |
50 | |
51 | SEAD_SINGLETON_DISPOSER_IMPL(Manager) |
52 | |
53 | Manager::Manager() : mVersionFile(), mGdtReinitSlot{this, &Manager::onGdtReinit} { |
54 | resetFlags(); |
55 | } |
56 | |
57 | Manager::~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 | |
78 | void 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 | |
86 | void 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 | |
105 | void 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 | |
119 | void 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 | |
136 | void Manager::registerAocMainFieldPack() { |
137 | mAocMainFieldPack.waitForReady(); |
138 | mAocMainFieldPack.parseResource(nullptr); |
139 | if (mAocMainFieldPack.isSuccess()) |
140 | mAocMainFieldPackPrefix.registerPrefix("Aoc/0010/" , mAocMainFieldPack.getResource(), false); |
141 | } |
142 | |
143 | void Manager::unloadAocMainFieldPack() { |
144 | mAocMainFieldPackPrefix.deregister(); |
145 | mAocMainFieldPack.requestUnload(); |
146 | } |
147 | |
148 | sead::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 | |
169 | bool 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 | |
200 | sead::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 | |
217 | sead::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 | |
227 | sead::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 | |
244 | sead::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 | |
254 | sead::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 | |
264 | sead::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 | |
274 | bool 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 | |
305 | static 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 | |
313 | bool 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 | |
320 | bool 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 | |
342 | bool Manager::isAocField(const sead::SafeString& map_type) { |
343 | return map_type == "AocField" ; |
344 | } |
345 | |
346 | sead::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 | |
366 | void 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 | |
375 | bool 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 | |
389 | bool 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 | |
406 | bool 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 |
454 | void 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 | |
473 | bool 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 | |
490 | void Manager::initGameData() { |
491 | reinitFlags(); |
492 | ksys::gdt::Manager::instance()->addReinitCallback(mGdtReinitSlot); |
493 | } |
494 | |
495 | static 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 | |
503 | void 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 | |
527 | void 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 | |
542 | void Manager::onGdtReinit(ksys::gdt::Manager::ReinitEvent* event) { |
543 | reinitFlags(); |
544 | } |
545 | |
546 | } // namespace uking::aoc |
547 | |