diff --git a/src/d/actor/d_a_alink_demo.inc b/src/d/actor/d_a_alink_demo.inc index 42651dfd27..d0dda1b36f 100644 --- a/src/d/actor/d_a_alink_demo.inc +++ b/src/d/actor/d_a_alink_demo.inc @@ -23,6 +23,8 @@ #include "d/actor/d_a_npc_tkc.h" #include +#include "dusk/archipelago/archipelago_context.hpp" + #if TARGET_PC #include "dusk/imgui/ImGuiConsole.hpp" #include "dusk/settings.h" @@ -3302,6 +3304,15 @@ int daAlink_c::procCoDeadInit(int param_0) { field_0x3080 = 0; mProcVar5.field_0x3012 = 0x3F; field_0x3198 = -1; + +#if TARGET_PC + // trigger archipelago death link if applicable + if (dusk::archi::ArchipelagoContext::IsConnected()) { + dusk::archi::ArchipelagoContext::TryHandleDeathLink(); + } + +#endif + return 1; } diff --git a/src/d/d_file_sel_info.cpp b/src/d/d_file_sel_info.cpp index f99ff7ef4c..63df8cbd0b 100644 --- a/src/d/d_file_sel_info.cpp +++ b/src/d/d_file_sel_info.cpp @@ -16,6 +16,7 @@ #include "dusk/string.hpp" #include "dusk/version.hpp" +#include "dusk/archipelago/archipelago_context.hpp" dFile_info_c::dFile_info_c(JKRArchive* i_archive, u8 param_1) { mArchive = i_archive; @@ -120,9 +121,11 @@ int dFile_info_c::setSaveData(dSv_save_c* i_savedata, BOOL i_validChksum, u8 i_d // If this is a randomizer file auto curFileSeedHash = dusk::getSettings().randomizer.seedHashes.at(i_dataNo).getValue(); if (!curFileSeedHash.empty()) { - // Overwrite "Save time" text with "Randomizer" + bool isArchipelago = dusk::archi::ArchipelagoContext::IsSeedHashArchipelago(curFileSeedHash); + + // Overwrite "Save time" text with "Randomizer" or "Archipelago" auto saveTimeText = (J2DTextBox*)mFileInfo.Scr->search(MULTI_CHAR('f_s_t_02')); - dusk::SafeStringCopy(saveTimeText->getStringPtr(), "Randomizer"); + dusk::SafeStringCopy(saveTimeText->getStringPtr(), isArchipelago ? "Archipelago" : "Randomizer"); saveTimeText->setHBinding(J2DTextBoxHBinding::HBIND_LEFT); // Overwrite the "Total play time" text with the seed hash diff --git a/src/dusk/archipelago/archipelago_context.cpp b/src/dusk/archipelago/archipelago_context.cpp index 2207851fdf..0964c4bbcf 100644 --- a/src/dusk/archipelago/archipelago_context.cpp +++ b/src/dusk/archipelago/archipelago_context.cpp @@ -4,6 +4,7 @@ #include "Archipelago.h" #include "d/d_item.h" +#include "d/actor/d_a_alink.h" #include "dusk/config.hpp" #include "dusk/logging.h" #include "dusk/randomizer/game/tools.h" @@ -136,6 +137,17 @@ void ParseMessageData() { auto msg = AP_GetLatestMessage(); switch (msg->type) { + case AP_MessageType::ItemSend: { + auto sendMsg = (AP_ItemSendMessage*)msg; + ui::push_toast({ + .title = "Item Sent", + .content = fmt::format("Sent {} to {}", sendMsg->item, sendMsg->recvPlayer), + .duration = std::chrono::seconds(3), + }); + + DuskLog.info("[{}] {}", getMessageTypeName(msg->type), msg->text); + break; + } case AP_MessageType::ItemRecv: { auto recvMsg = (AP_ItemRecvMessage*)msg; @@ -147,7 +159,6 @@ void ParseMessageData() { // fallthrough for debug logging text contents } case AP_MessageType::Plaintext: - case AP_MessageType::ItemSend: case AP_MessageType::Hint: case AP_MessageType::Countdown: DuskLog.info("[{}] {}", getMessageTypeName(msg->type), msg->text); @@ -266,6 +277,45 @@ std::string ArchipelagoContext::getLocationNameFromApId(int apId) const { return ""; } +bool ArchipelagoContext::tryKillPlayer() { + if (!m_isNeedPlayerDeath) + return false; + + auto linkActor = daAlink_getAlinkActorClass(); + + if (!linkActor) + return false; + + switch (linkActor->mProcID) { + case daAlink_c::PROC_WAIT: + case daAlink_c::PROC_TIRED_WAIT: + case daAlink_c::PROC_MOVE: + case daAlink_c::PROC_WOLF_WAIT: + case daAlink_c::PROC_WOLF_TIRED_WAIT: + case daAlink_c::PROC_WOLF_MOVE: + case daAlink_c::PROC_ATN_MOVE: + case daAlink_c::PROC_WOLF_ATN_AC_MOVE: { + // Check if link is currently in a cutscene + if (linkActor->checkEventRun()) + break; + + // Ensure that link is not currently in a message-based event. + if (linkActor->getEventId() != 0) + break; + + dComIfGs_setLife(0); + + m_isNeedPlayerDeath = false; + + return true; + } + default: + break; + } + + return false; +} + ArchipelagoContext::ArchipelagoContext() = default; void ArchipelagoContext::SetServerIp(const std::string_view& ip) { @@ -295,7 +345,7 @@ const std::string& ArchipelagoContext::GetPassword() { std::string ArchipelagoContext::GetArchipelagoSeedName() { if (IsConnected()) { auto& roomInfo = instance().m_roomInfo; - return fmt::format("AP_{}_{}", GetSlotName(), roomInfo.seed_name); + return fmt::format("AP_{}", roomInfo.seed_name); }else { DuskLog.fatal("Archipelago was not connected when attempting to get seed name!"); } @@ -332,6 +382,13 @@ bool ArchipelagoContext::ConnectToServer(bool isBlocking) { AP_NetworkVersion ver{0, 6,7}; AP_SetClientVersion(&ver); + AP_SetDeathLinkSupported(true); + + AP_SetDeathLinkRecvCallback([](std::string source, std::string cause) { + DuskLog.info("Player {} sent death link. Cause: {}", source, cause); + RequestPlayerDeath(true); + }); + AP_SetItemClearCallback([]() { DuskLog.info("Item Clear Callback Called!"); instance().m_isNeedResetInv = true; @@ -395,14 +452,11 @@ bool ArchipelagoContext::IsConnected() { } 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()) { AP_GetRoomInfo(&instance().m_roomInfo); + instance().m_isEnableDeathLink = AP_IsDeathLinkEnabled(); RequestAllLocationScout(); } @@ -425,6 +479,12 @@ void ArchipelagoContext::Execute() { return; // end execution early so next frame can re-add inventory if needed } + // process death links + if (instance().tryKillPlayer()) { + // if successful, don't bother processing item queue or location checks + return; + } + // drain pending item queue here instance().m_queueMutex.lock(); if (!instance().m_receivedItemsQueue.empty()) { @@ -541,7 +601,8 @@ void ArchipelagoContext::HandleReceiveLocationScout(const std::vectorGetName(); if (!instance().m_locationItemInfo.contains(locName)) { - DuskLog.warn("No item found for ({}).", locName); + DuskLog.debug("No item found for ({}).", locName); continue; } @@ -659,6 +720,13 @@ bool ArchipelagoContext::IsReceivedLocationScouts() { return !instance().m_locationItemInfo.empty(); } +void ArchipelagoContext::TryHandleDeathLink() { + if (instance().m_isEnableDeathLink && !instance().m_isFromDeathLink) { + // TODO: come up with better death messages + AP_DeathLinkSend("%YOU% was unable to become the Hero of Twilight."); + } +} + void ArchipelagoContext::RequestAllLocationScout(bool isHint) { std::set locations; // TEMP: apworld has 475 locations with ids in sequential order, so add them all individually to location set @@ -670,6 +738,11 @@ void ArchipelagoContext::RequestAllLocationScout(bool isHint) { AP_SendLocationScouts(locations, isHint); } +void ArchipelagoContext::RequestPlayerDeath(bool isDeathLink) { + instance().m_isNeedPlayerDeath = true; + instance().m_isFromDeathLink = isDeathLink; +} + bool ArchipelagoContext::GenerateConfigFromAP(randomizer::seedgen::config::Config& config, const std::string& settingsStr) { YAML::Node apConfigYaml; try { diff --git a/src/dusk/archipelago/archipelago_context.hpp b/src/dusk/archipelago/archipelago_context.hpp index 8ce5efcb30..b2b8660d19 100644 --- a/src/dusk/archipelago/archipelago_context.hpp +++ b/src/dusk/archipelago/archipelago_context.hpp @@ -37,12 +37,15 @@ namespace dusk::archi bool m_isUpdateLocations = false; bool m_isNeedResetInv = false; bool m_isAllowUpdateLocations = false; + bool m_isEnableDeathLink = false; // AP Data std::unordered_map m_locationItemInfo; std::map m_initLocationCollectState; AP_RoomInfo m_roomInfo; std::string m_SettingsFile; + bool m_isNeedPlayerDeath = false; + bool m_isFromDeathLink = false; // TEMP std::map m_apItemToGameItem; @@ -57,6 +60,8 @@ namespace dusk::archi int getItemIdFromApId(int apId); std::string getLocationNameFromApId(int apId) const; + + bool tryKillPlayer(); public: ArchipelagoContext(); @@ -112,10 +117,14 @@ namespace dusk::archi static bool IsReceivedLocationScouts(); + static void TryHandleDeathLink(); + // State Requesters static void RequestAllLocationScout(bool isHint = false); + static void RequestPlayerDeath(bool isDeathLink = false); + // AP -> Internal Rando Converters static bool GenerateConfigFromAP(randomizer::seedgen::config::Config& config, const std::string& settingsStr); diff --git a/src/dusk/imgui/ImGuiArchipelagoDebug.cpp b/src/dusk/imgui/ImGuiArchipelagoDebug.cpp index b43c7f3f88..7a83904f51 100644 --- a/src/dusk/imgui/ImGuiArchipelagoDebug.cpp +++ b/src/dusk/imgui/ImGuiArchipelagoDebug.cpp @@ -62,6 +62,16 @@ void ImGuiArchipelagoDebug::drawWindow() { } } + ImGui::SeparatorText("Debug Buttons"); + + static bool isSimulateDeathLink = false; + + if (ImGui::Button("Request Death Link")) { + archi::ArchipelagoContext::RequestPlayerDeath(isSimulateDeathLink); + } + ImGui::SameLine(); + ImGui::Checkbox("Simulate Death Link", &isSimulateDeathLink); + ImGui::End(); } } \ No newline at end of file diff --git a/src/dusk/randomizer/generator/data/text/languages/english.yaml b/src/dusk/randomizer/generator/data/text/languages/english.yaml index df186835f5..fcfe619a12 100644 --- a/src/dusk/randomizer/generator/data/text/languages/english.yaml +++ b/src/dusk/randomizer/generator/data/text/languages/english.yaml @@ -288,6 +288,14 @@ Progressive Wallet: Cryptic: Text: a {money bag} +Archipelago Item: + Standard: + Text: Archipelago Item + Pretty: + Text: an {Archipelago Item} + Cryptic: + Text: an {item from another world} + Upper Zoras River Portal: Standard: Text: Upper Zoras River Portal @@ -1604,6 +1612,11 @@ No Required Dungeons Text: Text: No Required Dungeons # ITEM GET TEXT +Archipelago Item Get Item Text: + Standard: + Text: |- + You got an Archipelago Item! + Foolish Get Item Text: Standard: Text: |- diff --git a/src/dusk/randomizer/generator/data/text/languages/french.yaml b/src/dusk/randomizer/generator/data/text/languages/french.yaml index 4d0e30a146..6e23f2cc33 100644 --- a/src/dusk/randomizer/generator/data/text/languages/french.yaml +++ b/src/dusk/randomizer/generator/data/text/languages/french.yaml @@ -288,6 +288,14 @@ Progressive Wallet: Cryptic: Text: a {money bag} +Archipelago Item: + Standard: + Text: Archipelago Item + Pretty: + Text: an {Archipelago Item} + Cryptic: + Text: an {item from another world} + Upper Zoras River Portal: Standard: Text: Upper Zoras River Portal @@ -1604,6 +1612,11 @@ No Required Dungeons Text: Text: No Required Dungeons # ITEM GET TEXT +Archipelago Item Get Item Text: + Standard: + Text: |- + You got an Archipelago Item! + Foolish Get Item Text: Standard: Text: |- diff --git a/src/dusk/randomizer/generator/data/text/languages/german.yaml b/src/dusk/randomizer/generator/data/text/languages/german.yaml index c3310a286c..fe6d8d9460 100644 --- a/src/dusk/randomizer/generator/data/text/languages/german.yaml +++ b/src/dusk/randomizer/generator/data/text/languages/german.yaml @@ -288,6 +288,14 @@ Progressive Wallet: Cryptic: Text: a {money bag} +Archipelago Item: + Standard: + Text: Archipelago Item + Pretty: + Text: an {Archipelago Item} + Cryptic: + Text: an {item from another world} + Upper Zoras River Portal: Standard: Text: Upper Zoras River Portal @@ -1604,6 +1612,11 @@ No Required Dungeons Text: Text: No Required Dungeons # ITEM GET TEXT +Archipelago Item Get Item Text: + Standard: + Text: |- + You got an Archipelago Item! + Foolish Get Item Text: Standard: Text: |- diff --git a/src/dusk/randomizer/generator/data/text/languages/italian.yaml b/src/dusk/randomizer/generator/data/text/languages/italian.yaml index 057b380393..99821fa95d 100644 --- a/src/dusk/randomizer/generator/data/text/languages/italian.yaml +++ b/src/dusk/randomizer/generator/data/text/languages/italian.yaml @@ -288,6 +288,14 @@ Progressive Wallet: Cryptic: Text: a {money bag} +Archipelago Item: + Standard: + Text: Archipelago Item + Pretty: + Text: an {Archipelago Item} + Cryptic: + Text: an {item from another world} + Upper Zoras River Portal: Standard: Text: Upper Zoras River Portal @@ -1604,6 +1612,11 @@ No Required Dungeons Text: Text: No Required Dungeons # ITEM GET TEXT +Archipelago Item Get Item Text: + Standard: + Text: |- + You got an Archipelago Item! + Foolish Get Item Text: Standard: Text: |- diff --git a/src/dusk/randomizer/generator/data/text/languages/spanish.yaml b/src/dusk/randomizer/generator/data/text/languages/spanish.yaml index 38dd5e9ec8..9aa880e042 100644 --- a/src/dusk/randomizer/generator/data/text/languages/spanish.yaml +++ b/src/dusk/randomizer/generator/data/text/languages/spanish.yaml @@ -288,6 +288,14 @@ Progressive Wallet: Cryptic: Text: a {money bag} +Archipelago Item: + Standard: + Text: Archipelago Item + Pretty: + Text: an {Archipelago Item} + Cryptic: + Text: an {item from another world} + Upper Zoras River Portal: Standard: Text: Upper Zoras River Portal @@ -1604,6 +1612,11 @@ No Required Dungeons Text: Text: No Required Dungeons # ITEM GET TEXT +Archipelago Item Get Item Text: + Standard: + Text: |- + You got an Archipelago Item! + Foolish Get Item Text: Standard: Text: |- diff --git a/src/dusk/randomizer/generator/data/text/text_overrides.yaml b/src/dusk/randomizer/generator/data/text/text_overrides.yaml index 3ab5245d8a..bac0dc0a64 100644 --- a/src/dusk/randomizer/generator/data/text/text_overrides.yaml +++ b/src/dusk/randomizer/generator/data/text/text_overrides.yaml @@ -11,6 +11,10 @@ # Group: 0 # Message Id: 122 +- Name: Archipelago Item Get Item Text + Group: 0 + Message Id: 321 + - Name: Shadow Crystal Get Item Text Group: 0 Message Id: 151 diff --git a/src/dusk/randomizer/randomizer.cmake b/src/dusk/randomizer/randomizer.cmake index 1a6107fd93..5e9e619c80 100644 --- a/src/dusk/randomizer/randomizer.cmake +++ b/src/dusk/randomizer/randomizer.cmake @@ -56,7 +56,7 @@ message(STATUS "randomizer: Fetching APCpp") FetchContent_Declare( APCpp GIT_REPOSITORY https://github.com/CraftyBoss/APCpp.git - GIT_TAG 3557bbb + GIT_TAG 2d92f75 ) FetchContent_MakeAvailable(yaml-cpp base64pp battery-embed APCpp)