From 6366ab61d1b6fdd799d2b0ca4fa35054032f6c49 Mon Sep 17 00:00:00 2001 From: CraftyBoss Date: Fri, 19 Jun 2026 17:07:42 -0700 Subject: [PATCH] add inventory resetting on server connect, properly handle getting new items from locations, integrate some of archi into new rando ui stuff For the most part, solo archipelago runs seem to work fine now using the original TP apworld, but im sure there are plenty of weird issues still that need to be handled. --- include/d/d_com_inf_game.h | 32 + include/d/d_save.h | 3 + src/d/d_com_inf_game.cpp | 40 ++ src/d/d_file_select.cpp | 40 +- src/d/d_item.cpp | 5 +- src/d/d_save.cpp | 26 + src/dusk/archipelago/archipelago_context.cpp | 575 +++++++++++++++--- src/dusk/archipelago/archipelago_context.hpp | 60 +- src/dusk/imgui/ImGuiArchipelagoDebug.cpp | 10 +- src/dusk/imgui/ImGuiMenuRandomizer.cpp | 14 +- .../randomizer/game/randomizer_context.cpp | 19 +- .../randomizer/game/randomizer_context.hpp | 7 + src/dusk/randomizer/game/tools.cpp | 76 +++ src/dusk/randomizer/game/tools.h | 5 + src/dusk/randomizer/generator/data/items.yaml | 7 +- .../randomizer/generator/logic/entrance.cpp | 2 +- .../randomizer/generator/logic/entrance.hpp | 2 +- src/dusk/randomizer/generator/logic/hints.cpp | 99 +-- src/dusk/randomizer/generator/logic/hints.hpp | 1 + src/dusk/randomizer/generator/logic/world.cpp | 14 +- src/dusk/randomizer/generator/logic/world.hpp | 2 +- src/dusk/randomizer/generator/randomizer.cpp | 24 +- src/dusk/randomizer/generator/randomizer.hpp | 2 +- src/dusk/randomizer/randomizer.cmake | 4 +- 24 files changed, 894 insertions(+), 175 deletions(-) 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)