diff --git a/include/d/d_com_inf_game.h b/include/d/d_com_inf_game.h index 3bbdb4f449..2337a15ba5 100644 --- a/include/d/d_com_inf_game.h +++ b/include/d/d_com_inf_game.h @@ -1128,6 +1128,15 @@ void dComIfGs_setWarpItemData(char const* stage, cXyz pos, s16 angle, s8 roomNo, u8 param_5); BOOL dComIfGs_isStageSwitch(int i_stageNo, int i_no); BOOL dComIfGs_isStageTbox(int i_stageNo, int i_no); +#if TARGET_PC +void dComIfGs_onStageTbox(int i_stageNo, int i_no); +void dComIfGs_offStageTbox(int i_stageNo, int i_no); + +void dComIfGs_onStageItem(int i_stageNo, int i_no); +void dComIfGs_offStageItem(int i_stageNo, int i_no); + +#endif + void dComIfGs_onStageSwitch(int i_stageNo, int i_no); void dComIfGs_offStageSwitch(int i_stageNo, int i_no); BOOL dComIfGs_isStageSwitch(int i_stageNo, int i_no); @@ -1990,6 +1999,23 @@ inline void dComIfGs_onRegionFlag(int i_stageNo, int i_no) { const int shift = i_no % 8; regionFlags[offset] |= (0x80 >> shift); } + +inline void dComIfGs_onSaveTbox(int i_stageNo, int i_no) { + g_dComIfG_gameInfo.info.getSavedata().getSave(i_stageNo).getBit().onTbox(i_no); +} + +inline void dComIfGs_offSaveTbox(int i_stageNo, int i_no) { + g_dComIfG_gameInfo.info.getSavedata().getSave(i_stageNo).getBit().offTbox(i_no); +} + +inline void dComIfGs_onSaveItem(int i_no) { + g_dComIfG_gameInfo.info.getMemory().getBit().onItem(i_no); +} + +inline void dComIfGs_offSaveItem(int i_no) { + g_dComIfG_gameInfo.info.getMemory().getBit().offItem(i_no); +} + #endif inline BOOL dComIfGs_isSaveTbox(int i_stageNo, int i_no) { @@ -2456,6 +2482,12 @@ inline void dComIfGs_onItem(int i_bitNo, int i_roomNo) { g_dComIfG_gameInfo.info.onItem(i_bitNo, i_roomNo); } +#if TARGET_PC +inline void dComIfGs_offItem(int i_bitNo, int i_roomNo) { + g_dComIfG_gameInfo.info.offItem(i_bitNo, i_roomNo); +} +#endif + inline bool dComIfGs_isItem(int i_bitNo, int i_roomNo) { return g_dComIfG_gameInfo.info.isItem(i_bitNo, i_roomNo); } diff --git a/include/d/d_save.h b/include/d/d_save.h index be09b95f6e..1e789db798 100644 --- a/include/d/d_save.h +++ b/include/d/d_save.h @@ -993,6 +993,9 @@ public: BOOL isSwitch(int i_no, int i_roomNo) const; BOOL revSwitch(int i_no, int i_roomNo); void onItem(int i_no, int i_roomNo); +#if TARGET_PC + void offItem(int i_no, int i_roomNo); +#endif BOOL isItem(int i_no, int i_roomNo) const; void onActor(int i_no, int i_roomNo); void offActor(int i_no, int i_roomNo); diff --git a/src/d/d_com_inf_game.cpp b/src/d/d_com_inf_game.cpp index db6d243f9a..99bf94dd59 100644 --- a/src/d/d_com_inf_game.cpp +++ b/src/d/d_com_inf_game.cpp @@ -2093,6 +2093,41 @@ BOOL dComIfGs_isStageTbox(int i_stageNo, int i_no) { } } +#if TARGET_PC +void dComIfGs_onStageTbox(int i_stageNo, int i_no) { + if (dComIfGp_getStageStagInfo() && i_stageNo == dStage_stagInfo_GetSaveTbl(dComIfGp_getStageStagInfo())) { + dComIfGs_onTbox(i_no); + } else { + dComIfGs_onSaveTbox(i_stageNo, i_no); + } +} +void dComIfGs_offStageTbox(int i_stageNo, int i_no) { + if (dComIfGp_getStageStagInfo() && i_stageNo == dStage_stagInfo_GetSaveTbl(dComIfGp_getStageStagInfo())) { + dComIfGs_offTbox(i_no); + } else { + dComIfGs_offSaveTbox(i_stageNo, i_no); + } +} + +void dComIfGs_onStageItem(int i_stageNo, int i_no) { + if (dComIfGp_getStageStagInfo() && i_stageNo == dStage_stagInfo_GetSaveTbl(dComIfGp_getStageStagInfo())) { + dComIfGs_onItem(i_no, -1); + } else { + dComIfGs_onSaveItem(i_no); + } +} + +void dComIfGs_offStageItem(int i_stageNo, int i_no) { + if (dComIfGp_getStageStagInfo() && i_stageNo == dStage_stagInfo_GetSaveTbl(dComIfGp_getStageStagInfo())) { + // Need to subtract 0x80 (MEMORY_ITEM constant in d_save.cpp) because the above function does it + dComIfGs_offItem(i_stageNo, i_no - 0x80); + } else { + dComIfGs_offSaveItem(i_no); + } +} +#endif + + void dComIfGs_onStageSwitch(int i_stageNo, int i_no) { #if TARGET_PC // Avoid trying to get the save table if stag info is NULL @@ -2107,7 +2142,12 @@ void dComIfGs_onStageSwitch(int i_stageNo, int i_no) { } void dComIfGs_offStageSwitch(int i_stageNo, int i_no) { +#if TARGET_PC + // Avoid trying to get the save table if stag info is NULL + if (dComIfGp_getStageStagInfo() && i_stageNo == dStage_stagInfo_GetSaveTbl(dComIfGp_getStageStagInfo())) { +#else if (i_stageNo == dStage_stagInfo_GetSaveTbl(dComIfGp_getStageStagInfo())) { +#endif dComIfGs_offSwitch(i_no, -1); } diff --git a/src/d/d_file_select.cpp b/src/d/d_file_select.cpp index 93ac1f91d6..6424e2ab7f 100644 --- a/src/d/d_file_select.cpp +++ b/src/d/d_file_select.cpp @@ -23,6 +23,8 @@ #include "m_Do/m_Do_graphic.h" #include +#include "dusk/archipelago/archipelago_context.hpp" + #if TARGET_PC #include "dusk/config.hpp" #include "dusk/menu_pointer.h" @@ -1037,18 +1039,23 @@ void dFile_select_c::dataSelectStart() { makeRecInfo(mSelectNum); #if TARGET_PC - // Load the randomizer seed if one is tied to this file - auto curFileSeedHash = dusk::getSettings().randomizer.seedHashes.at(mSelectNum).getValue(); - // If this is a vanilla file, clear rando data structures - if (curFileSeedHash.empty()) { - g_randomizerState = RandomizerState(); - randomizer_GetContext() = RandomizerContext(); - } - // Reset randomizer state if we're switching to a different file - else if (curFileSeedHash != randomizer_GetContext().mHash || g_randomizerState.mFileNum != mSelectNum) { - g_randomizerState = RandomizerState(); - randomizer_GetContext() = RandomizerContext(); - randomizer_GetContext().LoadFromHash(curFileSeedHash); + // TODO: not this please D: + if (dusk::archi::ArchipelagoContext::IsConnected()) { + dusk::archi::ArchipelagoContext::GenerateLocalWorldData(); + }else { + // Load the randomizer seed if one is tied to this file + auto curFileSeedHash = dusk::getSettings().randomizer.seedHashes.at(mSelectNum).getValue(); + // If this is a vanilla file, clear rando data structures + if (curFileSeedHash.empty()) { + g_randomizerState = RandomizerState(); + randomizer_GetContext() = RandomizerContext(); + } + // Reset randomizer state if we're switching to a different file + else if (curFileSeedHash != randomizer_GetContext().mHash || g_randomizerState.mFileNum != mSelectNum) { + g_randomizerState = RandomizerState(); + randomizer_GetContext() = RandomizerContext(); + randomizer_GetContext().LoadFromHash(curFileSeedHash); + } } #endif @@ -1401,6 +1408,15 @@ void dFile_select_c::selectDataPlayTypeMove() { modal.hide(true); dusk::ui::push_document(std::make_unique(this)); }}, + // If Archipelago is selected, open archipelago settings window + dusk::ui::ModalAction{ + .label = "Archipelago", + .onPressed = [this](dusk::ui::Modal& modal) { + mDusk.mBackToFileSelect = false; + mDoAud_seStartMenu(Z2SE_SY_CURSOR_OK); + modal.hide(true); + dusk::archi::ArchipelagoContext::GenerateLocalWorldData(); + }}, }, // If we dismiss this modal, go back to file selection .onDismiss = [this](dusk::ui::Modal& modal) { diff --git a/src/d/d_item.cpp b/src/d/d_item.cpp index a785f4bfac..48362938df 100644 --- a/src/d/d_item.cpp +++ b/src/d/d_item.cpp @@ -553,6 +553,7 @@ void execItemGet(u8 i_itemNo) { if (randomizer_IsActive()) { i_itemNo = verifyProgressiveItem(i_itemNo); g_randomizerState.mUpdateTracker = true; + dusk::archi::ArchipelagoContext::SetNeedUpdateLocations(true); } #endif @@ -2184,9 +2185,7 @@ void item_func_MIRROR_PIECE_1() { } void item_func_ARCHIPELAGO_ITEM() { - if (randomizer_IsActive()) { - dusk::archi::ArchipelagoContext::UpdateCheckedLocations(); - } + } #endif diff --git a/src/d/d_save.cpp b/src/d/d_save.cpp index ec6b8925ee..a87df764f7 100644 --- a/src/d/d_save.cpp +++ b/src/d/d_save.cpp @@ -2068,6 +2068,32 @@ void dSv_info_c::onItem(int i_no, int i_roomNo) { } } +#if TARGET_PC +void dSv_info_c::offItem(int i_no, int i_roomNo) { + JUT_ASSERT(4398, (0 <= i_no && i_no < (MEMORY_ITEM+ DAN_ITEM+ ZONE_ITEM+ ONEZONE_ITEM)) || i_no == -1 || i_no == 255); + + if (i_no == -1 || i_no == 255) { + return; + } + + if (i_no < MEMORY_ITEM) { + mDan.offItem(i_no); + } else if (i_no < (MEMORY_ITEM + DAN_ITEM)) { + mMemory.getBit().offItem(i_no - MEMORY_ITEM); + } else { + JUT_ASSERT(4414, 0 <= i_roomNo && i_roomNo < 64); + int zoneNo = dComIfGp_roomControl_getZoneNo(i_roomNo); + JUT_ASSERT(4416, 0 <= zoneNo && zoneNo < ZONE_MAX); + + if (i_no < (MEMORY_ITEM + DAN_ITEM + ZONE_ITEM)) { + mZone[zoneNo].getBit().offItem(i_no - (MEMORY_ITEM + DAN_ITEM)); + } else { + mZone[zoneNo].getBit().offOneItem(i_no - (MEMORY_ITEM + DAN_ITEM + ZONE_ITEM)); + } + } +} +#endif + BOOL dSv_info_c::isItem(int i_no, int i_roomNo) const { JUT_ASSERT(4488, (0 <= i_no && i_no < (MEMORY_ITEM+ DAN_ITEM+ ZONE_ITEM+ ONEZONE_ITEM)) || i_no == -1 || i_no == 255); diff --git a/src/dusk/archipelago/archipelago_context.cpp b/src/dusk/archipelago/archipelago_context.cpp index c944c91e66..d5d6808a09 100644 --- a/src/dusk/archipelago/archipelago_context.cpp +++ b/src/dusk/archipelago/archipelago_context.cpp @@ -7,6 +7,9 @@ #include "dusk/config.hpp" #include "dusk/logging.h" #include "dusk/randomizer/game/tools.h" +#include "dusk/randomizer/game/verify_item_functions.h" +#include "dusk/randomizer/generator/logic/hints.hpp" +#include "dusk/ui/rando_config.hpp" #include "dusk/ui/ui.hpp" namespace dusk::archi @@ -39,7 +42,7 @@ static auto sArchiSettingToDusklight = std::to_array({ {"lanayru_twilight_cleared", "Lanayru Twilight Cleared"}, {"skip_mdh", "Skip Midna's Desparate Hour"}, {"open_map", "Unlock Map Regions"}, - {"increase_wallet", "Increase Wallet Capacity"}, + {"increase_wallet", "Logic Increase Wallet Capacity"}, {"transform_anywhere", "Logic Transform Anywhere"}, {"bonks_do_damage", "Bonks Do Damage"}, {"skip_lakebed_entrance", "Lakebed Does Not Require Water Bombs"}, @@ -128,6 +131,20 @@ void ArchipelagoContext::LoadTempItemInfo() { itemName }; } + + // add temporary replacement IDs for items not included in the base rando + + m_apItemToGameItem[16] = { // Water Bombs (3) + 0x16, + randomizer::logic::item::Importance::JUNK, + "Water Bombs 5" + }; + + m_apItemToGameItem[20] = { // Bomblings (3) + 0x1A, + randomizer::logic::item::Importance::JUNK, + "Bomblings 5" + }; } void ArchipelagoContext::LoadTempLocationInfo() { @@ -164,15 +181,19 @@ void ArchipelagoContext::itemRecvImpl(int id, bool notify) { return; } + m_isAllowUpdateLocations = true; // guards against triggering UpdateCheckedLocations + auto& item = m_apItemToGameItem[id]; - DuskLog.info("[AP] Adding Item: {}", item.itemName); - - if (item.importance == randomizer::logic::item::Importance::MAJOR) { - g_randomizerState.addItemToEventQueue(item.itemId); + if (notify && item.importance == randomizer::logic::item::Importance::MAJOR) { + DuskLog.info("[AP] Adding Item: {}", item.itemName); + g_randomizerState.addItemToEventQueue(verifyProgressiveItem(item.itemId)); }else { + DuskLog.info("[AP] Silently Adding Item: {}", item.itemName); execItemGet(item.itemId); } + + m_isAllowUpdateLocations = false; } int ArchipelagoContext::getItemIdFromApId(int apId) { @@ -194,9 +215,7 @@ std::string ArchipelagoContext::getLocationNameFromApId(int apId) const { return ""; } -ArchipelagoContext::ArchipelagoContext() { - -} +ArchipelagoContext::ArchipelagoContext() = default; void ArchipelagoContext::SetServerIp(const std::string_view& ip) { getSettings().archipelago.serverIP.setValue(std::string(ip)); @@ -222,6 +241,22 @@ const std::string& ArchipelagoContext::GetPassword() { return getSettings().archipelago.serverPass.getValue(); } +std::string ArchipelagoContext::GetArchipelagoSeedName() { + if (IsConnected()) { + auto& roomInfo = instance().m_roomInfo; + return fmt::format("AP_{}_{}", GetSlotName(), roomInfo.seed_name); + }else { + DuskLog.fatal("Archipelago was not connected when attempting to get seed name!"); + } +} + +void ArchipelagoContext::GetSeedDirectoryPath(std::filesystem::path& outPath) { + if (IsConnected()) { + auto& roomInfo = instance().m_roomInfo; + outPath = ui::GetRandomizerPath() / "archipelago" / GetArchipelagoSeedName(); + } +} + void ArchipelagoContext::ConnectToServer() { config::Save(); @@ -236,16 +271,17 @@ void ArchipelagoContext::ConnectToServer() { AP_SetItemClearCallback([]() { DuskLog.info("Item Clear Callback Called!"); - // HandleResetInventory(); + instance().m_isNeedResetInv = true; }); - AP_SetItemRecvCallback([](int id, bool notify) { - DuskLog.info("Item Receive Callback Called! Item: {} Notify: {}", id, notify); - HandleItemReceived(id, notify); + AP_SetItemRecvCallback([](AP_NetworkItem& item, bool notify) { + DuskLog.info("Item Receive Callback Called! Item: {} Notify: {}", item.item, notify); + HandleItemReceived(item, notify); }); AP_SetLocationCheckedCallback([](int loc) { DuskLog.info("Location Checked Callback Called! Location: {}", loc); + SetLocationChecked(loc, true); }); AP_SetLocationInfoCallback([](std::vector items) { @@ -260,7 +296,7 @@ void ArchipelagoContext::ConnectToServer() { return; } - std::thread messageThread = std::thread(MainThreadFunc); + std::thread messageThread = std::thread(MessageThreadFunc); messageThread.detach(); } @@ -278,62 +314,82 @@ bool ArchipelagoContext::IsConnected() { return status == AP_ConnectionStatus::Connected || status == AP_ConnectionStatus::Authenticated; } -void ArchipelagoContext::MainThreadFunc() { +void ArchipelagoContext::MessageThreadFunc() { // wait a bit before checking connection state, as websocket is probably not connected yet // (i really am not liking APCpp, why cant I check if the websocket is in the process of connecting???) std::this_thread::sleep_for(std::chrono::seconds(2)); DuskLog.info("AP Thread started."); - if (IsConnected()) + if (IsConnected()) { + AP_GetRoomInfo(&instance().m_roomInfo); RequestAllLocationScout(); + } while (IsConnected()) { if (AP_IsMessagePending()) ParseMessageData(); - - instance().m_queueMutex.lock(); - if (randomizer_IsActive() && !instance().m_inactiveItemsQueue.empty()) { - for (auto item : instance().m_inactiveItemsQueue) { - instance().itemRecvImpl(item, false); - } - - instance().m_inactiveItemsQueue.clear(); - } - instance().m_queueMutex.unlock(); } DuskLog.info("AP Thread ended."); } -void ArchipelagoContext::HandleItemReceived(int id, bool notify) { - int relativeId = id - ARCHI_ITEM_OFFSET; +void ArchipelagoContext::Execute() { + if (!IsConnected()) + return; - // && ((relativeId >= 0 && relativeId <= 6) || relativeId == 7) - if (!notify) { + // reset player inventory if server requested it + if (instance().m_isNeedResetInv) { + HandleResetInventory(); + instance().m_isNeedResetInv = false; + return; // end execution early so next frame can re-add inventory if needed + } + + // drain pending item queue here + instance().m_queueMutex.lock(); + if (!instance().m_receivedItemsQueue.empty()) { + for (auto item : instance().m_receivedItemsQueue) { + instance().itemRecvImpl(item.first, item.second); + } + + instance().m_receivedItemsQueue.clear(); + } + instance().m_queueMutex.unlock(); + + // update location checks here if we need to + if (instance().m_isUpdateLocations) { + UpdateCheckedLocations(); + instance().m_isUpdateLocations = false; + } +} + +void ArchipelagoContext::HandleItemReceived(AP_NetworkItem& netItem, bool notify) { + int relativeId = netItem.item - ARCHI_ITEM_OFFSET; + + if (!notify && ((relativeId >= 0 && relativeId <= 6) || relativeId == 7)) { // skip rupee refills so players cant abuse disconnect/reconnect return; } - if (!randomizer_IsActive()) { - DuskLog.info("Randomizer not active, adding item to queue."); - - instance().m_queueMutex.lock(); - instance().m_inactiveItemsQueue.push_back(relativeId); - instance().m_queueMutex.unlock(); + if (netItem.location != -1 && IsLocationChecked(netItem.location)) { + // no need to handle item if its location has already been checked return; } - instance().itemRecvImpl(relativeId, notify); + instance().m_queueMutex.lock(); + instance().m_receivedItemsQueue.push_back({relativeId, notify}); + instance().m_queueMutex.unlock(); } void ArchipelagoContext::HandleResetInventory() { + DuskLog.info("Resetting Inventory."); // NOTE: this does not clear ALL things from save, so if a player managed to do something while disconnected from the archi, it might mess with things auto& playerInfo = g_dComIfG_gameInfo.info.getPlayer(); // reset items playerInfo.getItem().init(); + playerInfo.getGetItem().init(); // reset collect (poes, shards, swords) playerInfo.getCollect().init(); @@ -342,13 +398,46 @@ void ArchipelagoContext::HandleResetInventory() { playerInfo.getPlayerStatusA().setWalletSize(WALLET); // dont reset rupees, and instead reject rupee updates while refilling inv + // sync all location collect flags with current collection status obtained from initial room connection + UpdateAllLocationState(); + + // clear all item-related flags + + dComIfGs_offEventBit(0x2580); // Power up dominion rod + + // shadow crystal + dComIfGs_offEventBit(0xD04); // Can transform at will + dComIfGs_offEventBit(0x501); // Midna Charge Unlocked + + // hidden skills + dComIfGs_offEventBit(0x2904); // ENDING BLOW + dComIfGs_offEventBit(0x2908); // SHIELD ATTACK + dComIfGs_offEventBit(0x2902); // BACK SLICE + dComIfGs_offEventBit(0x2901); // HELM SPLITTER + dComIfGs_offEventBit(0x2A80); // MORTAL DRAW + dComIfGs_offEventBit(0x2A40); // JUMP STRIKE + dComIfGs_offEventBit(0x2A20); // GREAT SPIN + } void ArchipelagoContext::HandleReceiveLocationScout(const std::vector& items) { for (const auto& item : items) { - int parsedItemId = dItemNo_Randomizer_ARCHIPELAGO_ITEM_e; + int parsedItemId; + std::string parsedItemName; if (item.player == AP_GetPlayerID()) { - parsedItemId = instance().getItemIdFromApId(item.item - ARCHI_ITEM_OFFSET); + int adjustedId = item.item - ARCHI_ITEM_OFFSET; + + if (instance().m_apItemToGameItem.contains(adjustedId)) { + auto& itemInfo = instance().m_apItemToGameItem[adjustedId]; + parsedItemId = itemInfo.itemId; + parsedItemName = itemInfo.itemName; + }else { + parsedItemId = -1; + parsedItemName = "Unknown"; + } + }else { + parsedItemId = dItemNo_Randomizer_ARCHIPELAGO_ITEM_e; + parsedItemName = "Archipelago Item"; } int locationId = item.location - ARCHI_ITEM_OFFSET; @@ -359,26 +448,31 @@ void ArchipelagoContext::HandleReceiveLocationScout(const std::vector world = std::make_unique(1, nullptr); + bool changed = false; for (auto location : world->GetAllLocations()) { + // skip locations that aren't progression, which are locations that just aren't randomized + if (!location->IsProgression()) { + continue; + } + auto locName = location->GetName(); if (!instance().m_locationItemInfo.contains(locName)) { @@ -393,8 +487,92 @@ void ArchipelagoContext::UpdateCheckedLocations() { if (isCollected && !cachedLocData.collected) { cachedLocData.collected = true; AP_SendItem(cachedLocData.apLocationId); + changed = true; } } + + if (!changed) { + DuskLog.warn("No locations had any changes! this might not be normal."); + } +} + +void ArchipelagoContext::SetNeedUpdateLocations(bool update) { + if (!instance().m_isAllowUpdateLocations) + instance().m_isUpdateLocations = update; +} + +bool ArchipelagoContext::IsLocationChecked(int locId) { + auto& world = instance().m_archiWorld; + + for (const auto& [locName, locInfo] : instance().m_locationItemInfo) { + if (locInfo.apLocationId == locId) { + if (locInfo.collected) + return true; + + if (auto location = world->GetLocation(locInfo.locationName, true)) { + return isLocationObtained(location); + } + + DuskLog.error("Failed to obtain location: {}", locName); + return false; + } + } + return false; +} + +void ArchipelagoContext::SetLocationChecked(int locId, bool collected) { + // func was ran before location scouts could be sent out, cache result until scouts return. + if (instance().m_locationItemInfo.empty()) { + instance().m_initLocationCollectState[locId] = collected; + return; + } + + auto& world = instance().m_archiWorld; + + for (auto& [locName, locInfo] : instance().m_locationItemInfo) { + if (locInfo.apLocationId == locId) { + locInfo.collected = collected; + + // update location flags if possible + auto location = world->GetLocation(locInfo.locationName, true); + if (!location || !location->IsProgression()) + return; + + setLocationCollected(location, collected); + return; + } + } + + DuskLog.warn("No location found for locId {}.", locId); +} + +void ArchipelagoContext::UpdateLocationState(int locId, bool collected) { + auto& world = instance().m_archiWorld; + + for (const auto& [locName, locInfo] : instance().m_locationItemInfo) { + if (locInfo.apLocationId == locId) { + auto location = world->GetLocation(locInfo.locationName, true); + if (!location || !location->IsProgression()) + continue; + + setLocationCollected(location, collected); + return; + } + } + + DuskLog.warn("No location found for locId {}.", locId); +} + +void ArchipelagoContext::UpdateAllLocationState() { + auto& world = instance().m_archiWorld; + + for (const auto& [locName, locInfo] : instance().m_locationItemInfo) { + auto location = world->GetLocation(locInfo.locationName, true); + if (!location || !location->IsProgression()) + continue; + + setLocationCollected(location, locInfo.collected); + } } void ArchipelagoContext::RequestAllLocationScout(bool isHint) { @@ -412,7 +590,7 @@ void ArchipelagoContext::SetAPConfigYamlPath(const std::string_view& path) { instance().m_apConfigPath = path; } -bool ArchipelagoContext::GenerateConfigFromAP(randomizer::seedgen::config::Config& outConfig) { +bool ArchipelagoContext::GenerateConfigFromAP(randomizer::seedgen::config::Config& config) { if (instance().m_apConfigPath.empty()) { DuskLog.warn("AP Config Path Empty!"); return false; @@ -431,17 +609,8 @@ bool ArchipelagoContext::GenerateConfigFromAP(randomizer::seedgen::config::Confi return false; } - outConfig.SetSeed("Archipelago"); - - randomizer::seedgen::settings::Settings settings; - - auto defaultSettings = randomizer::seedgen::settings::GetAllSettingsInfo(); - - // add default settings to config first - for (const auto& [name, info] : *defaultSettings) { - if (info->GetType() == randomizer::seedgen::settings::Type::STANDARD) - settings.InsertSetting(name, randomizer::seedgen::settings::Setting(info.get(), info->GetDefaultOption())); - } + config.SetSeed("Archipelago"); + randomizer::seedgen::settings::Settings& settings = config.GetSettings(); // update settings using ap config for (const auto& apSettingEntry : apConfigYaml["Twilight Princess"]) { @@ -470,11 +639,16 @@ bool ArchipelagoContext::GenerateConfigFromAP(randomizer::seedgen::config::Confi auto& setting = settings.GetMap().at(settingConvert.dusklightName); - if (apSettingValue) { - setting.SetCurrentOption("On"); - }else { - setting.SetCurrentOption("Off"); - } + setting.SetCurrentOption(apSettingValue ? "On" : "Off"); + + continue; + } + if (apSettingName == "poe_shuffled") { + auto& setting = settings.GetMap().at("Poe Souls"); + bool apSettingValue = apSettingEntry.second.as(); + + // this setting has more options, but the current apworld only has off or on for now. + setting.SetCurrentOption(apSettingValue ? "All" : "Vanilla"); continue; } @@ -482,16 +656,152 @@ bool ArchipelagoContext::GenerateConfigFromAP(randomizer::seedgen::config::Confi auto apSettingValue = apSettingEntry.second.as(); - // TODO: the rest of the translations + // TODO: clean up this if-else hellscape if (apSettingName == "castle_requirements") { + auto& setting = settings.GetMap().at("Hyrule Barrier Requirements"); + // ap assumes max mirror shards/fused shadows/dungeons, so update those settings as well + + if(apSettingValue == "open") + setting.SetCurrentOption("Open"); + else if(apSettingValue == "vanilla") + setting.SetCurrentOption("Vanilla"); + else if(apSettingValue == "fused_shadows") { + setting.SetCurrentOption("Fused Shadows"); + settings.GetMap().at("Hyrule Barrier Fused Shadows").SetCurrentOption("3"); + }else if(apSettingValue == "mirror_shards") { + setting.SetCurrentOption("Mirror Shards"); + settings.GetMap().at("Hyrule Barrier Mirror Shards").SetCurrentOption("4"); + }else if(apSettingValue == "all_dungeons") { + setting.SetCurrentOption("Dungeons"); + settings.GetMap().at("Hyrule Barrier Dungeons").SetCurrentOption("8"); + } + }else if (apSettingName == "palace_requirements") { + auto& setting = settings.GetMap().at("Palace of Twilight Requirements"); + + if(apSettingValue == "open") + setting.SetCurrentOption("Open"); + else if(apSettingValue == "vanilla") + setting.SetCurrentOption("Vanilla"); + else if(apSettingValue == "fused_shadows") + setting.SetCurrentOption("Fused Shadows"); + else if(apSettingValue == "mirror_shards") + setting.SetCurrentOption("Mirror Shards"); + + }else if (apSettingName == "faron_woods_logic") { + auto& setting = settings.GetMap().at("Faron Woods Logic"); + + if(apSettingValue == "open") + setting.SetCurrentOption("Open"); + else if(apSettingValue == "closed") + setting.SetCurrentOption("Closed"); + }else if (apSettingName == "small_key_settings") { + auto& setting = settings.GetMap().at("Small Keys"); + + if(apSettingValue == "vanilla") + setting.SetCurrentOption("Vanilla"); + else if(apSettingValue == "own_dungeon") + setting.SetCurrentOption("Own Dungeon"); + else if(apSettingValue == "any_dungeon") + setting.SetCurrentOption("Any Dungeon"); + else if(apSettingValue == "anywhere") + setting.SetCurrentOption("Anywhere"); + else if(apSettingValue == "startwith") + setting.SetCurrentOption("Keysy"); + + }else if (apSettingName == "big_key_settings") { + auto& setting = settings.GetMap().at("Big Keys"); + + if(apSettingValue == "vanilla") + setting.SetCurrentOption("Vanilla"); + else if(apSettingValue == "own_dungeon") + setting.SetCurrentOption("Own Dungeon"); + else if(apSettingValue == "any_dungeon") + setting.SetCurrentOption("Any Dungeon"); + else if(apSettingValue == "anywhere") + setting.SetCurrentOption("Anywhere"); + else if(apSettingValue == "startwith") + setting.SetCurrentOption("Keysy"); + + }else if (apSettingName == "map_and_compass_settings") { + auto& setting = settings.GetMap().at("Maps and Compasses"); + + if(apSettingValue == "vanilla") + setting.SetCurrentOption("Vanilla"); + else if(apSettingValue == "own_dungeon") + setting.SetCurrentOption("Own Dungeon"); + else if(apSettingValue == "any_dungeon") + setting.SetCurrentOption("Any Dungeon"); + else if(apSettingValue == "anywhere") + setting.SetCurrentOption("Anywhere"); + else if(apSettingValue == "startwith") + setting.SetCurrentOption("Keysy"); + + }else if (apSettingName == "trap_frequency") { + auto& setting = settings.GetMap().at("Trap Item Frequency"); + + if(apSettingValue == "no_traps") + setting.SetCurrentOption("None"); + else if(apSettingValue == "few") + setting.SetCurrentOption("Few"); + else if(apSettingValue == "many") + setting.SetCurrentOption("Many"); + else if(apSettingValue == "mayhem") + setting.SetCurrentOption("Mayhem"); + else if(apSettingValue == "nightmare") + setting.SetCurrentOption("Nightmare"); + + }else if (apSettingName == "damage_magnification") { + auto& setting = settings.GetMap().at("Logic Damage Multiplier"); + + if(apSettingValue == "vanilla") + setting.SetCurrentOption("Vanilla"); + else if(apSettingValue == "double") + setting.SetCurrentOption("Double"); + else if(apSettingValue == "triple") + setting.SetCurrentOption("Triple"); + else if(apSettingValue == "quadruple") + setting.SetCurrentOption("Quadruple"); + else if(apSettingValue == "ohko") + setting.SetCurrentOption("OHKO"); + + }else if (apSettingName == "goron_mines_entrance") { + auto& setting = settings.GetMap().at("Goron Mines Entrance"); + + if(apSettingValue == "closed") + setting.SetCurrentOption("Closed"); + else if(apSettingValue == "no_wrestling") + setting.SetCurrentOption("No Wrestling"); + else if(apSettingValue == "open") + setting.SetCurrentOption("Open"); + + }else if (apSettingName == "tot_entrance") { + auto& setting = settings.GetMap().at("Sacred Grove Does Not Require Skull Kid"); + auto& setting2 = settings.GetMap().at("Temple of Time Sword Requirement"); + + if(apSettingValue == "closed") { + setting.SetCurrentOption("Off"); + setting2.SetCurrentOption("Master Sword"); + }else if (apSettingValue == "open_grove") { + setting.SetCurrentOption("On"); + setting2.SetCurrentOption("Master Sword"); + }else if (apSettingValue == "open") { + setting.SetCurrentOption("On"); + setting2.SetCurrentOption("None"); + } + + }else if (apSettingName == "logic_rules") { + auto& setting = settings.GetMap().at("Logic Rules"); + + if(apSettingValue == "glitchless") { + setting.SetCurrentOption("All Locations Reachable"); + }else if (apSettingValue == "glitched") { // this might not be the most direct translation + setting.SetCurrentOption("Beatable Only"); + } } - } - outConfig.GetSettingsList().push_back(settings); - return true; } @@ -502,4 +812,133 @@ int ArchipelagoContext::GetItemAtLocation(const std::string& locName) { } return instance().m_locationItemInfo[locName].itemId; } + +int ArchipelagoContext::GetItemAtLocation(int locId) { + for (const auto& [locName, locInfo] : instance().m_locationItemInfo) { + if (locInfo.apLocationId == locId) { + return locInfo.itemId; + } + } + return 0; +} + +void ArchipelagoContext::CreateArchipelagoWorld() { + std::filesystem::path workingDir; + GetSeedDirectoryPath(workingDir); + + auto trackerRando = randomizer::Randomizer(workingDir); + trackerRando.GenerateTrackerWorld(false); + + instance().m_archiWorld = std::move(trackerRando.GetWorlds().front()); +} + +void ArchipelagoContext::FillArchipelagoWorld() { + auto& world = instance().m_archiWorld; + + if (world == nullptr) { + DuskLog.error("Archipelago world was not created!"); + return; + } + + auto& locationInfo = instance().m_locationItemInfo; + + // fill all locations with data pulled from archi session + for (auto location : world->GetAllLocations()) { + // skip locations that aren't progression, which are locations that just aren't randomized + if (!location->IsProgression()) { + location->SetCurrentItem(location->GetOriginalItem()); + continue; + } + + auto locName = location->GetName(); + if (!locationInfo.contains(locName)) { + if (!location->HasCategories("Warp Portal") && + !location->HasCategories("Placeholder") && + !location->HasCategories("Hint Sign")) + DuskLog.warn("Missing archipelago location data for: {}", locName); + auto origItem = location->GetOriginalItem(); + + // set location to original item + + if (origItem->GetID() != -1) // ensure item is not nothing + location->SetCurrentItem(origItem); + else + DuskLog.info("Location ({}) does not have an original item!", locName); + + continue; + } + + auto& locInfo = locationInfo[locName]; + if (locInfo.itemId != -1) { + location->SetCurrentItem(world->GetItem(locInfo.itemId)); + }else { + DuskLog.info("Skipping location ({}) as item is -1.", locName); + } + } +} + +void ArchipelagoContext::CreateRandomizerContext() { + auto& world = instance().m_archiWorld; + + // Set hint texts before writing context + randomizer::logic::hints::GenerateAllHints(world); + + // TODO: generate archipelago item get text replacements + + auto randoData = WriteSeedData(world.get()); + randoData.mHash = GetArchipelagoSeedName(); + + randomizer_GetContext() = randoData; + + std::filesystem::path workingDir; + GetSeedDirectoryPath(workingDir); + + auto writeToFileResult = randoData.WriteToFile(workingDir / "seed.dat"); + + if (writeToFileResult.has_value()) { + DuskLog.error("Failed to create Rando Data. Reason: {}", writeToFileResult.value()); + return; + } +} + +void ArchipelagoContext::LoadRandomizerContext() { + randomizer_GetContext() = RandomizerContext(); + + std::filesystem::path workingDir; + GetSeedDirectoryPath(workingDir); + + randomizer_GetContext().LoadFromPath(workingDir / "seed.dat"); + randomizer_GetContext().mHash = GetArchipelagoSeedName(); +} + +void ArchipelagoContext::GenerateLocalWorldData() { + bool createContext = false; + std::filesystem::path workingDir; + + GetSeedDirectoryPath(workingDir); + + if (std::filesystem::exists(workingDir)) { + instance().m_config.LoadFromFile(workingDir / "settings.yaml", workingDir / "preferences.yaml"); + }else { + std::filesystem::create_directories(workingDir); + // creates base yamls at directory if they dont exist yet + instance().m_config.LoadFromFile(workingDir / "settings.yaml", workingDir / "preferences.yaml"); + + GenerateConfigFromAP(instance().m_config); + + instance().m_config.WriteToFile(workingDir / "settings.yaml", workingDir / "preferences.yaml"); + + createContext = true; + } + + CreateArchipelagoWorld(); + + FillArchipelagoWorld(); + + if (createContext) { + CreateRandomizerContext(); + }else { + LoadRandomizerContext(); + } +} } // dusk::archi \ No newline at end of file diff --git a/src/dusk/archipelago/archipelago_context.hpp b/src/dusk/archipelago/archipelago_context.hpp index 5024f0abff..08a5174db3 100644 --- a/src/dusk/archipelago/archipelago_context.hpp +++ b/src/dusk/archipelago/archipelago_context.hpp @@ -10,26 +10,38 @@ namespace dusk::archi class ArchipelagoContext { private: struct TEMP_GameItemInfo { - int itemId; - randomizer::logic::item::Importance importance; + int itemId = -1; + randomizer::logic::item::Importance importance = randomizer::logic::item::Importance::INVALID; std::string itemName; }; struct TEMP_GameLocationInfo { - int apId; + int apId = -1; std::string locName; }; struct GameLocationInfo { - int itemId; - int64_t apLocationId; - bool collected; + int itemId = -1; + std::string itemName; + std::string locationName; + int64_t apLocationId = -1; + bool collected = false; }; - std::vector m_inactiveItemsQueue; + std::vector> m_receivedItemsQueue; std::mutex m_queueMutex; + // Rando Data + randomizer::seedgen::config::Config m_config; + std::unique_ptr m_archiWorld = nullptr; + bool m_isUpdateLocations = false; + bool m_isNeedResetInv = false; + bool m_isAllowUpdateLocations = false; + + // AP Data std::unordered_map m_locationItemInfo; + std::map m_initLocationCollectState; + AP_RoomInfo m_roomInfo; // TEMP std::map m_apItemToGameItem; @@ -58,6 +70,10 @@ namespace dusk::archi static const std::string& GetSlotName(); static const std::string& GetPassword(); + static std::string GetArchipelagoSeedName(); + + static void GetSeedDirectoryPath(std::filesystem::path& outPath); + // Connection Handlers static void ConnectToServer(); @@ -68,9 +84,11 @@ namespace dusk::archi // State Handlers - static void MainThreadFunc(); + static void MessageThreadFunc(); - static void HandleItemReceived(int id, bool notify); + static void Execute(); + + static void HandleItemReceived(AP_NetworkItem& id, bool notify); static void HandleResetInventory(); @@ -78,6 +96,16 @@ namespace dusk::archi static void UpdateCheckedLocations(); + static void SetNeedUpdateLocations(bool update); + + static bool IsLocationChecked(int locId); + + static void SetLocationChecked(int locId, bool collected); + + static void UpdateLocationState(int locId, bool collected); + + static void UpdateAllLocationState(); + // State Requesters static void RequestAllLocationScout(bool isHint = false); @@ -86,9 +114,21 @@ namespace dusk::archi static void SetAPConfigYamlPath(const std::string_view& path); - static bool GenerateConfigFromAP(randomizer::seedgen::config::Config& outConfig); + static bool GenerateConfigFromAP(randomizer::seedgen::config::Config& config); static int GetItemAtLocation(const std::string& locName); + static int GetItemAtLocation(int locId); + + static void CreateArchipelagoWorld(); + + static void FillArchipelagoWorld(); + + static void CreateRandomizerContext(); + + static void LoadRandomizerContext(); + + static void GenerateLocalWorldData(); + }; } // dusk::archi \ No newline at end of file diff --git a/src/dusk/imgui/ImGuiArchipelagoDebug.cpp b/src/dusk/imgui/ImGuiArchipelagoDebug.cpp index a17242aa92..5a1ed2243a 100644 --- a/src/dusk/imgui/ImGuiArchipelagoDebug.cpp +++ b/src/dusk/imgui/ImGuiArchipelagoDebug.cpp @@ -6,6 +6,7 @@ #include "aurora/lib/window.hpp" #include "dusk/file_select.hpp" #include "dusk/archipelago/archipelago_context.hpp" +#include "dusk/ui/rando_seed_generation.hpp" namespace dusk { @@ -69,12 +70,11 @@ void ImGuiArchipelagoDebug::drawWindow() { OpenApFilePicker(); } - if (ImGui::Button("Test Config Convert")) { - randomizer::seedgen::config::Config config; - archi::ArchipelagoContext::GenerateConfigFromAP(config); - } - if (archi::ArchipelagoContext::IsConnected()) { + if (ImGui::Button("Test Create World Data")) { + archi::ArchipelagoContext::GenerateLocalWorldData(); + } + if (ImGui::Button("Disconnect")) { archi::ArchipelagoContext::DisconnectFromServer(); } diff --git a/src/dusk/imgui/ImGuiMenuRandomizer.cpp b/src/dusk/imgui/ImGuiMenuRandomizer.cpp index 9ed99bf23b..0c733c00ce 100644 --- a/src/dusk/imgui/ImGuiMenuRandomizer.cpp +++ b/src/dusk/imgui/ImGuiMenuRandomizer.cpp @@ -20,6 +20,8 @@ #include #include +#include "dusk/archipelago/archipelago_context.hpp" + namespace dusk { @@ -254,8 +256,16 @@ namespace dusk { auto trackerHash = trackerRando->GetConfig().GetHash(false); // If no hash, or seeds switched, try to create tracker world from currently active seed if (trackerHash.empty() || (trackerHash != contextHash && !contextHash.empty())) { - *trackerRando = randomizer::Randomizer(ui::GetRandomizerPath()); - trackerRando->GenerateTrackerWorld(); + if (archi::ArchipelagoContext::IsConnected()) { + std::filesystem::path workingDir; + archi::ArchipelagoContext::GetSeedDirectoryPath(workingDir); + + *trackerRando = randomizer::Randomizer(workingDir); + trackerRando->GenerateTrackerWorld(false); + }else { + *trackerRando = randomizer::Randomizer(ui::GetRandomizerPath()); + trackerRando->GenerateTrackerWorld(); + } } if (randomizer_IsActive()) { diff --git a/src/dusk/randomizer/game/randomizer_context.cpp b/src/dusk/randomizer/game/randomizer_context.cpp index 64e159a50b..ddc377ea28 100644 --- a/src/dusk/randomizer/game/randomizer_context.cpp +++ b/src/dusk/randomizer/game/randomizer_context.cpp @@ -22,10 +22,14 @@ #include "d/d_meter2_draw.h" #include "d/d_meter2_info.h" #include "d/d_msg_flow.h" +#include "dusk/archipelago/archipelago_context.hpp" std::optional RandomizerContext::WriteToFile() { + return WriteToFile(this->GetSeedDataPath()); +} - std::ofstream seedData(this->GetSeedDataPath()); +std::optional RandomizerContext::WriteToFile(const fspath& path) { + std::ofstream seedData(path); if (!seedData.is_open()) { return "Could not open seed data file"; } @@ -137,7 +141,11 @@ std::optional RandomizerContext::LoadFromHash(const std::string& ha return std::nullopt; } - auto in = LoadYAML(this->GetSeedDataPath()); + return LoadFromPath(this->GetSeedDataPath()); +} + +std::optional RandomizerContext::LoadFromPath(const fspath& path) { + auto in = LoadYAML(path); // Necessary settings for (const auto& settingNode : in["mSettings"] ) { @@ -587,6 +595,8 @@ int RandomizerState::execute() { handleFoolishItem(); } + dusk::archi::ArchipelagoContext::Execute(); + return 1; } @@ -1001,6 +1011,11 @@ RandomizerContext WriteSeedData(randomizer::logic::world::World* world) { // Set data for all locations for (const auto& location : world->GetAllLocations()) { + // skip locations with nothing + if (location->GetCurrentItem()->GetID() == -1) { + continue; + } + const auto& metaData = location->GetMetadata(); // Chest Overrides diff --git a/src/dusk/randomizer/game/randomizer_context.hpp b/src/dusk/randomizer/game/randomizer_context.hpp index 7f5fcf8bf0..b857648b17 100644 --- a/src/dusk/randomizer/game/randomizer_context.hpp +++ b/src/dusk/randomizer/game/randomizer_context.hpp @@ -80,7 +80,9 @@ public: } mStartLocation; std::optional WriteToFile(); + std::optional WriteToFile(const fspath& path);; std::optional LoadFromHash(const std::string& hash); + std::optional LoadFromPath(const fspath& path); std::filesystem::path GetSeedDataPath() const; enum Settings { @@ -267,4 +269,9 @@ u32 getStageObjCRC32(u8* data, size_t size); */ bool GenerateAndWriteSeed(std::string& generationStatusMsg); +/* + * Creates RandomizerContext that contains all needed data for the seed. + */ +RandomizerContext WriteSeedData(randomizer::logic::world::World* world); + #endif //DUSK_RANDOMIZER_CONTEXT_HPP diff --git a/src/dusk/randomizer/game/tools.cpp b/src/dusk/randomizer/game/tools.cpp index 71791ba9d2..699b77027a 100644 --- a/src/dusk/randomizer/game/tools.cpp +++ b/src/dusk/randomizer/game/tools.cpp @@ -495,8 +495,84 @@ randomizer::logic::item_pool::ItemPool getSaveItemPool(randomizer::logic::world: return pool; } +void setLocationCollected(randomizer::logic::location::Location* location, bool collect) { + auto& locationMeta = location->GetMetadata(); + + if (auto& chestNode = locationMeta["Chest"]) { + auto tboxId = chestNode[0]["Tbox Id"].as(); + auto stageId = getStageSaveId(chestNode[0]["Stage"].as()); + if (collect) + dComIfGs_onStageTbox(stageId, tboxId); + else + dComIfGs_offStageTbox(stageId, tboxId); + } + if (auto& poeNode = locationMeta["Poe"]) { + auto flag = poeNode[0]["Flag"].as(); + auto stageId = getStageSaveId(poeNode[0]["Stage"].as()); + if (collect) + dComIfGs_onStageSwitch(stageId, flag); + else + dComIfGs_offStageSwitch(stageId, flag); + } + if (auto& freeStandingItemNode = locationMeta["Freestanding Item"]) { + auto flag = freeStandingItemNode[0]["Flag"].as(); + auto stageId = getStageSaveId(freeStandingItemNode[0]["Stage"].as()); + // big baba uses tbox, hardcode this edge case + if (location->GetName() == "Forest Temple Big Baba Key") { + if (collect) + dComIfGs_onStageTbox(stageId, flag); + else + dComIfGs_offStageTbox(stageId, flag); + }else { + if (collect) + dComIfGs_onStageItem(stageId, flag); + else + dComIfGs_offStageItem(stageId, flag); + } + } + if (auto& eventFlagNode = locationMeta["Event Flag"]) { + auto flag = eventFlagNode.as(); + if (collect) + dComIfGs_onEventBit(flag); + else + dComIfGs_offEventBit(flag); + } + if (auto& wolfNode = locationMeta["Golden Wolf"]) { + auto flag = wolfNode[0]["Flag"].as(); + if (collect) + dComIfGs_onEventBit(flag); + else + dComIfGs_offEventBit(flag); + } + if (auto& switchFlagNode = locationMeta["Switch Flag"]) { + auto flag = switchFlagNode["Flag"].as(); + auto stageId = getStageSaveId(switchFlagNode["Stage"].as()); + if (collect) + dComIfGs_onStageSwitch(stageId, flag); + else + dComIfGs_offStageSwitch(stageId, flag); + } + if (auto& itemFlagNode = locationMeta["Item Flag"]) { + auto flag = itemFlagNode["Flag"].as(); + auto stageId = getStageSaveId(itemFlagNode["Stage"].as()); + if (collect) + dComIfGs_onStageItem(stageId, flag); + else + dComIfGs_offStageItem(stageId, flag); + } + if (auto& twilitInsectNode = locationMeta["Twilit Insect"]) { + auto flag = twilitInsectNode[0]["Flag"].as(); + auto stageId = getStageSaveId(twilitInsectNode[0]["Stage"].as()); + if (collect) + dComIfGs_onStageTbox(stageId, flag); + else + dComIfGs_offStageTbox(stageId, flag); + } +} + bool isLocationObtained(randomizer::logic::location::Location* location) { auto& locationMeta = location->GetMetadata(); + if (auto& chestNode = locationMeta["Chest"]) { auto tboxId = chestNode[0]["Tbox Id"].as(); auto stageId = getStageSaveId(chestNode[0]["Stage"].as()); diff --git a/src/dusk/randomizer/game/tools.h b/src/dusk/randomizer/game/tools.h index 74995bd5a8..e782ca2e4a 100644 --- a/src/dusk/randomizer/game/tools.h +++ b/src/dusk/randomizer/game/tools.h @@ -25,6 +25,11 @@ int getTempleKeysFound(int saveId); */ randomizer::logic::item_pool::ItemPool getSaveItemPool(randomizer::logic::world::World* world); +/* + * Updates locations relevant flag in save to whatever state is supplied. + */ +void setLocationCollected(randomizer::logic::location::Location* location, bool collect); + /* * Finds locations relevant flag in save (using its metadata) and checks if it's been set. */ diff --git a/src/dusk/randomizer/generator/data/items.yaml b/src/dusk/randomizer/generator/data/items.yaml index fe4e1e8565..c36addc1bf 100644 --- a/src/dusk/randomizer/generator/data/items.yaml +++ b/src/dusk/randomizer/generator/data/items.yaml @@ -1055,9 +1055,10 @@ # Importance: Junk # Id: 0xDB -#- Name: Unused -# Importance: Junk -# Id: 0xDC +- Name: Archipelago Item + Importance: Minor + Id: 0xDC + APItemId: -1 #- Name: Unused # Importance: Junk diff --git a/src/dusk/randomizer/generator/logic/entrance.cpp b/src/dusk/randomizer/generator/logic/entrance.cpp index b27d8d2c47..67b4457abf 100644 --- a/src/dusk/randomizer/generator/logic/entrance.cpp +++ b/src/dusk/randomizer/generator/logic/entrance.cpp @@ -232,7 +232,7 @@ namespace randomizer::logic::entrance return this->_decoupled; } - void Entrance::SetDisbled(const bool& disabled) + void Entrance::SetDisabled(const bool& disabled) { this->_disabled = disabled; LOG_TO_DEBUG(this->GetOriginalName() + " disabled status set to " + (disabled ? "True" : "False")); diff --git a/src/dusk/randomizer/generator/logic/entrance.hpp b/src/dusk/randomizer/generator/logic/entrance.hpp index 8159d4353c..6d1e487397 100644 --- a/src/dusk/randomizer/generator/logic/entrance.hpp +++ b/src/dusk/randomizer/generator/logic/entrance.hpp @@ -102,7 +102,7 @@ namespace randomizer::logic::entrance bool IsShuffled() const; void SetDecoupled(const bool& decoupled); bool IsDecoupled() const; - void SetDisbled(const bool& disabled); + void SetDisabled(const bool& disabled); bool IsDisabled() const; void SetPrimary(const bool& primary); bool IsPrimary() const; diff --git a/src/dusk/randomizer/generator/logic/hints.cpp b/src/dusk/randomizer/generator/logic/hints.cpp index 7deff29e4e..01f4984a39 100644 --- a/src/dusk/randomizer/generator/logic/hints.cpp +++ b/src/dusk/randomizer/generator/logic/hints.cpp @@ -4,36 +4,33 @@ #include "world.hpp" namespace randomizer::logic::hints { + static const std::unordered_map dungeonColors = { + {"Forest Temple", ""}, + {"Goron Mines", ""}, + {"Lakebed Temple", ""}, + {"Arbiters Grounds", ""}, + {"Snowpeak Ruins", ""}, + {"Temple of Time", ""}, + {"City in the Sky", ""}, + {"Palace of Twilight", ""}, + // {"Hyrule Castle", ""} + }; // Tell the player which dungeons are required on the sign in front of Link's House - static void GenerateRequiredDungeonsHint(world::WorldPool& worlds) { - static const std::unordered_map dungeonColors = { - {"Forest Temple", ""}, - {"Goron Mines", ""}, - {"Lakebed Temple", ""}, - {"Arbiters Grounds", ""}, - {"Snowpeak Ruins", ""}, - {"Temple of Time", ""}, - {"City in the Sky", ""}, - {"Palace of Twilight", ""}, - // {"Hyrule Castle", ""} - }; - - for (const auto& world : worlds) { - auto& requiredDungeonText = world->AddNewText("Links House Sign"); - for (const auto& [dungeonName, dungeon] : world->GetDungeonTable()) { - if (dungeon->IsRequired()) { - requiredDungeonText += dungeonColors.at(dungeonName) + getTextObject(dungeonName) + "\n"; - } + static void GenerateRequiredDungeonsHint(world::World* world) { + auto& requiredDungeonText = world->AddNewText("Links House Sign"); + for (const auto& [dungeonName, dungeon] : world->GetDungeonTable()) { + if (dungeon->IsRequired()) { + requiredDungeonText += dungeonColors.at(dungeonName) + getTextObject(dungeonName) + "\n"; } + } - if (requiredDungeonText.Empty()) { - requiredDungeonText += getTextObject("No Required Dungeons Text"); - } + if (requiredDungeonText.Empty()) { + requiredDungeonText += getTextObject("No Required Dungeons Text"); } } - static void doItemTextReplacement(const std::unique_ptr& world, + static void doItemTextReplacement(world::World* world, const std::string& locationName, const std::list& textNames, Text::Color color) { @@ -48,40 +45,44 @@ namespace randomizer::logic::hints { } } - static void GenerateItemTextReplacements(world::WorldPool& worlds) { - for (const auto& world : worlds) { - doItemTextReplacement(world, "Fishing Hole Bottle", {"Fishing Hole Sign Text"}, Text::GREEN); - doItemTextReplacement(world, "Charlo Donation Blessing", {"Charlo Donation Ask Text"}, Text::GREEN); - doItemTextReplacement(world, "Sera Shop Slingshot", {"Slingshot Shop Text", - "Slingshot Shop Too Expensive Text", "Slingshot Shop Purchase Confirmation Text", - "Slingshot Shop After Purchase Text"}, Text::ORANGE); + static void GenerateItemTextReplacements(world::World* world) { + doItemTextReplacement(world, "Fishing Hole Bottle", {"Fishing Hole Sign Text"}, Text::GREEN); + doItemTextReplacement(world, "Charlo Donation Blessing", {"Charlo Donation Ask Text"}, Text::GREEN); + doItemTextReplacement(world, "Sera Shop Slingshot", {"Slingshot Shop Text", + "Slingshot Shop Too Expensive Text", "Slingshot Shop Purchase Confirmation Text", + "Slingshot Shop After Purchase Text"}, Text::ORANGE); - doItemTextReplacement(world, "Barnes Bomb Bag", {"Barnes Special Offer Text"}, Text::ORANGE); - doItemTextReplacement(world, "Kakariko Village Malo Mart Wooden Shield", {"Kakariko Malo Mart Wooden Shield Purchase Confirmation Text", - "Kakariko Malo Mart Wooden Shield Too Expensive Text", "Kakariko Malo Mart Wooden Shield Text"}, Text::ORANGE); + doItemTextReplacement(world, "Barnes Bomb Bag", {"Barnes Special Offer Text"}, Text::ORANGE); + doItemTextReplacement(world, "Kakariko Village Malo Mart Wooden Shield", {"Kakariko Malo Mart Wooden Shield Purchase Confirmation Text", + "Kakariko Malo Mart Wooden Shield Too Expensive Text", "Kakariko Malo Mart Wooden Shield Text"}, Text::ORANGE); - doItemTextReplacement(world, "Kakariko Village Malo Mart Hylian Shield", {"Kakariko Malo Mart Hylian Shield Purchase Confirmation Text", - "Kakariko Malo Mart Hylian Shield Too Expensive Text", "Kakariko Malo Mart Hylian Shield After Purchase Text", - "Kakariko Malo Mart Hylian Shield Text"}, Text::ORANGE); + doItemTextReplacement(world, "Kakariko Village Malo Mart Hylian Shield", {"Kakariko Malo Mart Hylian Shield Purchase Confirmation Text", + "Kakariko Malo Mart Hylian Shield Too Expensive Text", "Kakariko Malo Mart Hylian Shield After Purchase Text", + "Kakariko Malo Mart Hylian Shield Text"}, Text::ORANGE); - doItemTextReplacement(world, "Kakariko Village Malo Mart Red Potion", {"Kakariko Malo Mart Red Potion Too Expensive Text", - "Kakariko Malo Mart Red Potion Purchase Confirmation Text", "Kakariko Malo Mart Red Potion Text"}, Text::ORANGE); + doItemTextReplacement(world, "Kakariko Village Malo Mart Red Potion", {"Kakariko Malo Mart Red Potion Too Expensive Text", + "Kakariko Malo Mart Red Potion Purchase Confirmation Text", "Kakariko Malo Mart Red Potion Text"}, Text::ORANGE); - doItemTextReplacement(world, "Kakariko Village Malo Mart Hawkeye", {"Kakariko Malo Mart Hawkeye Purchase Confirmation Text", - "Kakariko Malo Mart Hawkeye Too Expensive Text", "Kakariko Malo Mart Hawkeye After Purchase Text", - "Kakariko Malo Mart Hawkeye Coming Soon Text", "Kakariko Malo Mart Hawkeye Text"}, Text::ORANGE); + doItemTextReplacement(world, "Kakariko Village Malo Mart Hawkeye", {"Kakariko Malo Mart Hawkeye Purchase Confirmation Text", + "Kakariko Malo Mart Hawkeye Too Expensive Text", "Kakariko Malo Mart Hawkeye After Purchase Text", + "Kakariko Malo Mart Hawkeye Coming Soon Text", "Kakariko Malo Mart Hawkeye Text"}, Text::ORANGE); - doItemTextReplacement(world, "Castle Town Malo Mart Magic Armor", {"Chudleys Shop Magic Armor Text", - "Castle Town Malo Mart Magic Armor After Purchase Text", "Castle Town Malo Mart Magic Armor Text", - "Castle Town Malo Mart Magic Armor Sold Out Text"}, Text::ORANGE); + doItemTextReplacement(world, "Castle Town Malo Mart Magic Armor", {"Chudleys Shop Magic Armor Text", + "Castle Town Malo Mart Magic Armor After Purchase Text", "Castle Town Malo Mart Magic Armor Text", + "Castle Town Malo Mart Magic Armor Sold Out Text"}, Text::ORANGE); - doItemTextReplacement(world, "Coro Bottle", {"Coro Bottle Offer 1 Text", - "Coro Bottle Offer 2 Text", "Coro Bottle Offer 3 Text", "Coro Bottle Offer 4 Text"}, Text::ORANGE); - } + doItemTextReplacement(world, "Coro Bottle", {"Coro Bottle Offer 1 Text", + "Coro Bottle Offer 2 Text", "Coro Bottle Offer 3 Text", "Coro Bottle Offer 4 Text"}, Text::ORANGE); } void GenerateAllHints(world::WorldPool& worlds) { - GenerateRequiredDungeonsHint(worlds); - GenerateItemTextReplacements(worlds); + for (const auto& world : worlds) { + GenerateAllHints(world); + } + } + + void GenerateAllHints(const std::unique_ptr& world) { + GenerateRequiredDungeonsHint(world.get()); + GenerateItemTextReplacements(world.get()); } } diff --git a/src/dusk/randomizer/generator/logic/hints.hpp b/src/dusk/randomizer/generator/logic/hints.hpp index 28e5484773..c1b75eb9b7 100644 --- a/src/dusk/randomizer/generator/logic/hints.hpp +++ b/src/dusk/randomizer/generator/logic/hints.hpp @@ -3,5 +3,6 @@ namespace randomizer::logic::hints { void GenerateAllHints(world::WorldPool& worldPool); + void GenerateAllHints(const std::unique_ptr& world); } \ No newline at end of file diff --git a/src/dusk/randomizer/generator/logic/world.cpp b/src/dusk/randomizer/generator/logic/world.cpp index 05bbc48eff..362114f499 100644 --- a/src/dusk/randomizer/generator/logic/world.cpp +++ b/src/dusk/randomizer/generator/logic/world.cpp @@ -842,7 +842,7 @@ namespace randomizer::logic::world // Disable the dungeon's starting entrances for (auto& entrance : dungeon->GetStartingEntrances()) { - entrance->SetDisbled(true); + entrance->SetDisabled(true); } // Run an accessibility search to see which locations inherently require accessing this dungeon @@ -864,7 +864,7 @@ namespace randomizer::logic::world // Re-enable the dungeon's entrances for (auto& entrance : dungeon->GetStartingEntrances()) { - entrance->SetDisbled(false); + entrance->SetDisabled(false); } } } @@ -916,7 +916,7 @@ namespace randomizer::logic::world // Disable the dungeon's starting entrances for (auto& entrance : dungeon->GetStartingEntrances()) { - entrance->SetDisbled(true); + entrance->SetDisabled(true); } // Check if the game is beatable, set dungeon as required if so. If the dungeon is not required and barren @@ -942,7 +942,7 @@ namespace randomizer::logic::world // Re-enable the dungeon's entrances for (auto& entrance : dungeon->GetStartingEntrances()) { - entrance->SetDisbled(false); + entrance->SetDisabled(false); } } } @@ -1169,11 +1169,13 @@ namespace randomizer::logic::world return this->_startingItemPool; } - location::Location* World::GetLocation(const std::string& name) + location::Location* World::GetLocation(const std::string& name, const bool& ignoreError) { if (!this->_locationTable.contains(name)) { - throw std::runtime_error("Unknown location name \"" + name + "\""); + if (!ignoreError) + throw std::runtime_error("Unknown location name \"" + name + "\""); + return nullptr; } return this->_locationTable.at(name).get(); } diff --git a/src/dusk/randomizer/generator/logic/world.hpp b/src/dusk/randomizer/generator/logic/world.hpp index 4b7beaacb7..17532b9a25 100644 --- a/src/dusk/randomizer/generator/logic/world.hpp +++ b/src/dusk/randomizer/generator/logic/world.hpp @@ -131,7 +131,7 @@ namespace randomizer::logic::world item::Item* GetGameWinningItem() const; item_pool::ItemPool& GetItemPool(); item_pool::ItemPool& GetStartingItemPool(); - location::Location* GetLocation(const std::string& name); + location::Location* GetLocation(const std::string& name, const bool& ignoreError = false); location::LocationPool GetAllLocations(const bool& includeNonItemLocations = false); area::Area* GetArea(const std::string& name, const bool& createIfNotFound = false); area::Area* GetRootArea() const; diff --git a/src/dusk/randomizer/generator/randomizer.cpp b/src/dusk/randomizer/generator/randomizer.cpp index ad2230cb25..31f222113e 100644 --- a/src/dusk/randomizer/generator/randomizer.cpp +++ b/src/dusk/randomizer/generator/randomizer.cpp @@ -15,6 +15,7 @@ #include #include "dusk/logging.h" +#include "dusk/archipelago/archipelago_context.hpp" #include "dusk/ui/rando_config.hpp" #include "dusk/randomizer/game/randomizer_context.hpp" @@ -45,19 +46,24 @@ namespace randomizer return std::nullopt; } - void Randomizer::GenerateTrackerWorld() { + void Randomizer::GenerateTrackerWorld(bool useAntiSpoilerLog) { auto contextHash = randomizer_GetContext().mHash; - if (contextHash.empty()) { - return; + if (!useAntiSpoilerLog) { + this->_config.LoadFromFile(GetConfigPath(), GetPrefPath()); + this->_config.SetHash(contextHash); + }else { + if (contextHash.empty()) { + return; + } + + std::filesystem::path seedSettings = dusk::ui::GetRandomizerSeedsPath() / + contextHash / (contextHash + " Anti-Spoiler Log.txt"); + + this->_config.LoadFromFile(seedSettings, GetPrefPath()); + this->_config.SetHash(contextHash); } - std::filesystem::path seedSettings = dusk::ui::GetRandomizerSeedsPath() / - contextHash / (contextHash + " Anti-Spoiler Log.txt"); - - this->_config.LoadFromFile(seedSettings, GetPrefPath()); - this->_config.SetHash(contextHash); - std::unique_ptr world = std::make_unique(1, this); world->SetSettings(this->_config.GetSettingsList().front()); // Always use logic when building a tracker world diff --git a/src/dusk/randomizer/generator/randomizer.hpp b/src/dusk/randomizer/generator/randomizer.hpp index ac743485f8..eec8e07aae 100644 --- a/src/dusk/randomizer/generator/randomizer.hpp +++ b/src/dusk/randomizer/generator/randomizer.hpp @@ -19,7 +19,7 @@ namespace randomizer */ std::optional Generate(); void GenerateWorlds(); - void GenerateTrackerWorld(); + void GenerateTrackerWorld(bool useAntiSpoilerLog = true); auto& GetConfig() { return this->_config; } auto& GetWorlds() { return this->_worlds; } diff --git a/src/dusk/randomizer/randomizer.cmake b/src/dusk/randomizer/randomizer.cmake index cd5e353ce8..97ca3d1588 100644 --- a/src/dusk/randomizer/randomizer.cmake +++ b/src/dusk/randomizer/randomizer.cmake @@ -55,8 +55,8 @@ FetchContent_Declare( message(STATUS "randomizer: Fetching APCpp") FetchContent_Declare( APCpp - GIT_REPOSITORY https://github.com/N00byKing/APCpp.git - GIT_TAG 9194179 + GIT_REPOSITORY https://github.com/CraftyBoss/APCpp.git + GIT_TAG 5091686 ) FetchContent_MakeAvailable(yaml-cpp base64pp battery-embed APCpp)