diff --git a/files.cmake b/files.cmake index 97333f9acf..d8e0f21ae9 100644 --- a/files.cmake +++ b/files.cmake @@ -1540,6 +1540,8 @@ set(DUSK_FILES src/dusk/ui/rando_config.hpp src/dusk/ui/rando_seed_generation.cpp src/dusk/ui/rando_seed_generation.hpp + src/dusk/ui/archi_connect_modal.cpp + src/dusk/ui/archi_connect_modal.hpp src/dusk/achievements.cpp src/dusk/iso_validate.cpp src/dusk/livesplit.cpp diff --git a/include/d/d_file_select.h b/include/d/d_file_select.h index eff7604f10..87a8952e2a 100644 --- a/include/d/d_file_select.h +++ b/include/d/d_file_select.h @@ -205,6 +205,7 @@ public: DATASELPROC_SELECT_DATA_NAME_MOVE, #if TARGET_PC DATASELPROC_SELECT_DATA_PLAY_MOVE, // Select between vanilla or randomizer play + DATASELPROC_MENU_ARCHIPELAGO_CONNECT, // Wait for archipelago to connect #endif DATASELPROC_SELECT_DATA_OPENERASE_MOVE, DATASELPROC_MENU_SELECT, @@ -336,6 +337,7 @@ public: void selectDataNameMove(); #if TARGET_PC void selectDataPlayTypeMove(); + void menuArchipelagoConnect(); #endif void selectDataOpenEraseMove(); void menuSelect(); @@ -742,6 +744,8 @@ public: dDlst_FileSelFade_c mFadeDlst; bool mStartNameAnm; bool mBackToFileSelect; + bool mArchipelagoBeginConnect; + bool mArchiStartCloseFile; int mPendingRmlCloseFrames{0}; } mDusk; #endif diff --git a/src/d/d_file_select.cpp b/src/d/d_file_select.cpp index 6424e2ab7f..4a3cb4aa65 100644 --- a/src/d/d_file_select.cpp +++ b/src/d/d_file_select.cpp @@ -24,6 +24,7 @@ #include #include "dusk/archipelago/archipelago_context.hpp" +#include "dusk/ui/archi_connect_modal.hpp" #if TARGET_PC #include "dusk/config.hpp" @@ -301,6 +302,7 @@ static DataSelProcFunc DataSelProc[] = { &dFile_select_c::selectDataNameMove, #if TARGET_PC &dFile_select_c::selectDataPlayTypeMove, + &dFile_select_c::menuArchipelagoConnect, #endif &dFile_select_c::selectDataOpenEraseMove, &dFile_select_c::menuSelect, @@ -1032,34 +1034,75 @@ void dFile_select_c::dataSelectStart() { ketteiTxtDispAnmInit(0); #endif +#if TARGET_PC + headerTxtSet(msgTbl[mSelectNum], 1, 0); + makeRecInfo(mSelectNum); + + // 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(); + mDataSelProc = DATASELPROC_SELECT_DATA_OPEN_MOVE; + + // ensure archipelago is not connected either + if (dusk::archi::ArchipelagoContext::IsConnected()) + dusk::archi::ArchipelagoContext::DisconnectFromServer(); + + selectDataMoveAnmInitSet(SelOpenStartFrameTbl[mSelectNum], SelOpenEndFrameTbl[mSelectNum]); + menuMoveAnmInitSet(799, 809); + selectWakuAlpahAnmInit(mSelectNum, 0xff, 0, g_fsHIO.select_box_appear_frames); + } + else if (dusk::archi::ArchipelagoContext::IsSeedHashArchipelago(curFileSeedHash)) { + if (dusk::archi::ArchipelagoContext::IsConnected()) { + // if connected, we need to check if we're already connected to the right slot/seed, + // if so, we can continue straight to file start, otherwise a disconnect must happen first. + if (!dusk::archi::ArchipelagoContext::IsCurrentSeedHash(curFileSeedHash)) { + mDataSelProc = DATASELPROC_MENU_ARCHIPELAGO_CONNECT; + mDusk.mArchipelagoBeginConnect = true; + mDusk.mArchiStartCloseFile = false; + }else { + mDataSelProc = DATASELPROC_SELECT_DATA_OPEN_MOVE; + + selectDataMoveAnmInitSet(SelOpenStartFrameTbl[mSelectNum], SelOpenEndFrameTbl[mSelectNum]); + menuMoveAnmInitSet(799, 809); + selectWakuAlpahAnmInit(mSelectNum, 0xff, 0, g_fsHIO.select_box_appear_frames); + } + }else { + mDataSelProc = DATASELPROC_MENU_ARCHIPELAGO_CONNECT; + mDusk.mArchipelagoBeginConnect = true; + mDusk.mArchiStartCloseFile = false; + } + }else { + // Reset randomizer state if we're switching to a different file + if (curFileSeedHash != randomizer_GetContext().mHash || g_randomizerState.mFileNum != mSelectNum) { + g_randomizerState = RandomizerState(); + randomizer_GetContext() = RandomizerContext(); + randomizer_GetContext().LoadFromHash(curFileSeedHash); + } + + // disconnect from archipelago if applicable + if (dusk::archi::ArchipelagoContext::IsConnected()) + dusk::archi::ArchipelagoContext::DisconnectFromServer(); + + selectDataMoveAnmInitSet(SelOpenStartFrameTbl[mSelectNum], SelOpenEndFrameTbl[mSelectNum]); + menuMoveAnmInitSet(799, 809); + selectWakuAlpahAnmInit(mSelectNum, 0xff, 0, g_fsHIO.select_box_appear_frames); + + mDataSelProc = DATASELPROC_SELECT_DATA_OPEN_MOVE; + } +#else + headerTxtSet(msgTbl[mSelectNum], 1, 0); selectDataMoveAnmInitSet(SelOpenStartFrameTbl[mSelectNum], SelOpenEndFrameTbl[mSelectNum]); menuMoveAnmInitSet(799, 809); selectWakuAlpahAnmInit(mSelectNum, 0xff, 0, g_fsHIO.select_box_appear_frames); makeRecInfo(mSelectNum); -#if TARGET_PC - // 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); - } - } + mDataSelProc = DATASELPROC_SELECT_DATA_OPEN_MOVE; #endif - mDataSelProc = DATASELPROC_SELECT_DATA_OPEN_MOVE; } modoruTxtDispAnmInit(1); @@ -1352,10 +1395,6 @@ void dFile_select_c::selectDataNameMove() { // Custom Proc to allow the player to select the play type and a randomizer seed // Initially copied and expanded upon from dFile_select_c::selectDataNameMove void dFile_select_c::selectDataPlayTypeMove() { - bool isHeaderTxtChange = headerTxtChangeAnm(); - bool isFileRecScale = fileRecScaleAnm2(); - bool isModoruTxtDisp = modoruTxtDispAnm(); - // If we want to start bringing in the name input if (mDusk.mStartNameAnm) { // Only do so when no documents are visible @@ -1383,8 +1422,8 @@ void dFile_select_c::selectDataPlayTypeMove() { } } // If the file select elements have disappeared, setup the play type modal - else if (isHeaderTxtChange == true && isFileRecScale == true && - isModoruTxtDisp == true) + else if (headerTxtChangeAnm() == true && fileRecScaleAnm2() == true && + modoruTxtDispAnm() == true) { // Push our modal for selecting the play type auto& playTypeModal = dusk::ui::push_document(std::make_unique(dusk::ui::Modal::Props{ @@ -1415,7 +1454,7 @@ void dFile_select_c::selectDataPlayTypeMove() { mDusk.mBackToFileSelect = false; mDoAud_seStartMenu(Z2SE_SY_CURSOR_OK); modal.hide(true); - dusk::archi::ArchipelagoContext::GenerateLocalWorldData(); + dusk::ui::BeginArchipelagoConnectionUI(true); }}, }, // If we dismiss this modal, go back to file selection @@ -1442,6 +1481,35 @@ void dFile_select_c::selectDataPlayTypeMove() { playTypeModal.focus(); } } + +void dFile_select_c::menuArchipelagoConnect() { + if (mDusk.mArchipelagoBeginConnect) { + dusk::ui::BeginArchipelagoConnectionUI(); + mDusk.mArchipelagoBeginConnect = false; + mDusk.mPendingRmlCloseFrames = 6; + }else if (mDusk.mArchiStartCloseFile) { + bool isHeaderTxtChange = headerTxtChangeAnm(); + bool isSelDataMove = selectDataMoveAnm(); + bool isKetteiTxtDisp = ketteiTxtDispAnm(); + + if (isHeaderTxtChange && isSelDataMove && isKetteiTxtDisp) { + mDataSelProc = DATASELPROC_SELECT_DATA_OPEN_MOVE; + mDusk.mArchipelagoBeginConnect = false; + mDusk.mArchiStartCloseFile = false; + } + }else if (!dusk::ui::any_document_visible()) { + if (mDusk.mPendingRmlCloseFrames > 0) { + mDusk.mPendingRmlCloseFrames -= 1; + } + if (mDusk.mPendingRmlCloseFrames == 0) { + mDusk.mArchiStartCloseFile = true; + + selectDataMoveAnmInitSet(SelOpenStartFrameTbl[mSelectNum], SelOpenEndFrameTbl[mSelectNum]); + menuMoveAnmInitSet(799, 809); + selectWakuAlpahAnmInit(mSelectNum, 0xff, 0, g_fsHIO.select_box_appear_frames); + } + } +} #endif void dFile_select_c::selectDataOpenEraseMove() { diff --git a/src/dusk/archipelago/archipelago_context.cpp b/src/dusk/archipelago/archipelago_context.cpp index d5d6808a09..c092e5c4ef 100644 --- a/src/dusk/archipelago/archipelago_context.cpp +++ b/src/dusk/archipelago/archipelago_context.cpp @@ -257,13 +257,25 @@ void ArchipelagoContext::GetSeedDirectoryPath(std::filesystem::path& outPath) { } } -void ArchipelagoContext::ConnectToServer() { +bool ArchipelagoContext::IsSeedHashArchipelago(const std::string& seedStr) { + return seedStr.starts_with("AP_"); +} + +bool ArchipelagoContext::IsCurrentSeedHash(const std::string& seedStr) { + return GetArchipelagoSeedName() == seedStr; +} + +bool ArchipelagoContext::ConnectToServer(bool isBlocking) { config::Save(); instance().LoadTempItemInfo(); instance().LoadTempLocationInfo(); + AP_SetLogCallback([](const std::string& msg) { + DuskLog.info("{}", msg); + }); + AP_Init(GetServerIp().c_str(), "Twilight Princess", GetSlotName().c_str(), GetPassword().c_str()); AP_NetworkVersion ver{0, 6,7}; @@ -291,21 +303,29 @@ void ArchipelagoContext::ConnectToServer() { AP_Start(); - if (AP_GetConnectionStatus() == AP_ConnectionStatus::ConnectionRefused) { - DuskLog.warn("Failed to Connect to Archipelago Server."); - return; + // above func spawns a websocket thread, but there isn't really a good way to ensure a connection + // attempt has been made except to wait for that thread to tick once + + if (isBlocking) { + // wait for ws thread to run a frame before checking for status + std::this_thread::sleep_for(std::chrono::seconds(1)); + + while (AP_GetConnectionStatus() == AP_ConnectionStatus::Connecting) + std::this_thread::yield(); + + if (!IsConnected()) { + DuskLog.error("Failed to connect to Archipelago Server!"); + return false; + } } std::thread messageThread = std::thread(MessageThreadFunc); messageThread.detach(); + + return true; } void ArchipelagoContext::DisconnectFromServer() { - if (!IsConnected()) { - DuskLog.warn("Attempted to disconnect from an already disconnected state!"); - return; - } - AP_Shutdown(); } @@ -522,7 +542,7 @@ bool ArchipelagoContext::IsLocationChecked(int locId) { 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()) { + if (!IsReceivedLocationScouts()) { instance().m_initLocationCollectState[locId] = collected; return; } @@ -575,6 +595,10 @@ void ArchipelagoContext::UpdateAllLocationState() { } } +bool ArchipelagoContext::IsReceivedLocationScouts() { + return !instance().m_locationItemInfo.empty(); +} + 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 diff --git a/src/dusk/archipelago/archipelago_context.hpp b/src/dusk/archipelago/archipelago_context.hpp index 08a5174db3..f0c5daad0b 100644 --- a/src/dusk/archipelago/archipelago_context.hpp +++ b/src/dusk/archipelago/archipelago_context.hpp @@ -74,9 +74,13 @@ namespace dusk::archi static void GetSeedDirectoryPath(std::filesystem::path& outPath); + static bool IsSeedHashArchipelago(const std::string& seedStr); + + static bool IsCurrentSeedHash(const std::string& seedStr); + // Connection Handlers - static void ConnectToServer(); + static bool ConnectToServer(bool isBlocking = false); static void DisconnectFromServer(); @@ -106,6 +110,8 @@ namespace dusk::archi static void UpdateAllLocationState(); + static bool IsReceivedLocationScouts(); + // State Requesters static void RequestAllLocationScout(bool isHint = false); diff --git a/src/dusk/randomizer/randomizer.cmake b/src/dusk/randomizer/randomizer.cmake index 97ca3d1588..1a6107fd93 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 5091686 + GIT_TAG 3557bbb ) FetchContent_MakeAvailable(yaml-cpp base64pp battery-embed APCpp) diff --git a/src/dusk/ui/archi_connect_modal.cpp b/src/dusk/ui/archi_connect_modal.cpp new file mode 100644 index 0000000000..724781f69a --- /dev/null +++ b/src/dusk/ui/archi_connect_modal.cpp @@ -0,0 +1,166 @@ +#include +#include + +#include + +#include "dusk/logging.h" +#include "dusk/archipelago/archipelago_context.hpp" +#include "m_Do/m_Do_audio.h" + +namespace dusk::ui { + + +static std::atomic connectStatus = ArchiConnectModal::ConnectionStatus::None; + +void CreateSetupConnectionInfoModal(); + +ArchiConnectModal::ArchiConnectModal() : + Modal({ + .title = "Archipelago", + .bodyRml = "Connecting to Server...", + .onDismiss = [this](Modal& modal) { + mDoAud_seStartMenu(kSoundWindowClose); + modal.pop(false); + }, + .icon = "verifying", + }) { + mRoot->SetProperty("white-space", "pre-line"); +} + +void ArchiConnectModal::update() { + Modal::update(); + + auto currentStatus = connectStatus.load(); + + if (currentStatus != mDisplayedStatus) { + mDisplayedStatus = currentStatus; + + if (currentStatus == ConnectionStatus::Success || + currentStatus == ConnectionStatus::Error) + { + if (currentStatus == ConnectionStatus::Success) { + mDoAud_seStartMenu(kSoundSeedGenerateSuccess); + set_icon("celebration"); + set_body("Sucessfully Connected to server!"); + } else { + mDoAud_seStartMenu(kSoundSeedGenerateError); + set_icon("error"); + set_body("Failed to Connect to server."); + } + add_action({ + .label = "OK", + .onPressed = [currentStatus](Modal& modal) { + mDoAud_seStartMenu(kSoundWindowClose); + modal.pop(false); + + // show connection setup modal on failure + if (currentStatus == ConnectionStatus::Error) { + CreateSetupConnectionInfoModal(); + } + } + }); + + // Refocus so that we focus the new button + focus(); + + mDisplayedStatus = ConnectionStatus::Ready; + }else if (currentStatus == ConnectionStatus::Generating) { + set_body("Loading seed data into game..."); + }else if (currentStatus == ConnectionStatus::Disconnecting) { + set_body("Disconnecting from previous server..."); + } + + connectStatus.store(mDisplayedStatus); + } +} + +void HandleArchipelagoConnect() { + // if a connection was already established, disconnect before attempting a new connection. + if (archi::ArchipelagoContext::IsConnected()) { + connectStatus.store(ArchiConnectModal::ConnectionStatus::Disconnecting); + archi::ArchipelagoContext::DisconnectFromServer(); + } + + if (!archi::ArchipelagoContext::ConnectToServer(true)) { + archi::ArchipelagoContext::DisconnectFromServer(); + connectStatus.store(ArchiConnectModal::ConnectionStatus::Error); + return; + } + + connectStatus.store(ArchiConnectModal::ConnectionStatus::Generating); + + while (!archi::ArchipelagoContext::IsReceivedLocationScouts()) { + std::this_thread::yield(); + } + + archi::ArchipelagoContext::GenerateLocalWorldData(); + + connectStatus.store(ArchiConnectModal::ConnectionStatus::Success); +} + +void ConnectAndLoadArchipelago() { + connectStatus.store(ArchiConnectModal::ConnectionStatus::Connecting); + std::thread archiConnectThread(HandleArchipelagoConnect); + archiConnectThread.detach(); + + push_document(std::make_unique()); + + if (auto* doc = top_document()) { + doc->focus(); + } +} + +void CreateSetupConnectionInfoModal() { + auto& doc = push_document(std::make_unique(Modal::Props{ + .title = "Connection Info", + .bodyRml = "", + .actions = { + ModalAction{ + .label = "Connect", + .onPressed = [](Modal& modal) { + auto textModal = dynamic_cast(&modal); + + archi::ArchipelagoContext::SetServerIp(textModal->get_input_text(0)); + archi::ArchipelagoContext::SetPassword(textModal->get_input_text(1)); + archi::ArchipelagoContext::SetSlotName(textModal->get_input_text(2)); + + modal.pop(false); + + ConnectAndLoadArchipelago(); + }, + }, + ModalAction{ + .label = "Cancel", + .onPressed = [](Modal& modal) { + // TODO: cancelling this modal will still progress file select to starting the save, + // which could potentially allow a user to break their save file. + + modal.pop(false); + }, + }, + }, + .icon = "information" + })); + auto inputModal = dynamic_cast(&doc); + + inputModal->add_input_text("Server IP", archi::ArchipelagoContext::GetServerIp()); + inputModal->add_input_text("Password", archi::ArchipelagoContext::GetPassword()); + inputModal->add_input_text("Slot Name", archi::ArchipelagoContext::GetSlotName()); +} + +void BeginArchipelagoConnectionUI(bool forceChangeConnection) { + if (forceChangeConnection) { + CreateSetupConnectionInfoModal(); + return; + } + + // TODO: associate server connection info to save slots + bool hasConnectInfo = (!archi::ArchipelagoContext::GetServerIp().empty() && !archi::ArchipelagoContext::GetSlotName().empty()); + + if (hasConnectInfo) { + ConnectAndLoadArchipelago(); + }else { + CreateSetupConnectionInfoModal(); + } +} +} \ No newline at end of file diff --git a/src/dusk/ui/archi_connect_modal.hpp b/src/dusk/ui/archi_connect_modal.hpp new file mode 100644 index 0000000000..7d36b72a83 --- /dev/null +++ b/src/dusk/ui/archi_connect_modal.hpp @@ -0,0 +1,29 @@ +#pragma once +#include "modal.hpp" + +namespace dusk::ui +{ + + + class ArchiConnectModal : public Modal { + public: + enum class ConnectionStatus { + None, + Ready, + Disconnecting, + Connecting, + Generating, + Success, + Error, + }; + + explicit ArchiConnectModal(); + void update() override; + + private: + ConnectionStatus mDisplayedStatus = ConnectionStatus::None; + }; + + void BeginArchipelagoConnectionUI(bool forceChangeConnection = false); + +} \ No newline at end of file diff --git a/src/dusk/ui/modal.cpp b/src/dusk/ui/modal.cpp index 0a50960a90..71638ff9f0 100644 --- a/src/dusk/ui/modal.cpp +++ b/src/dusk/ui/modal.cpp @@ -133,4 +133,67 @@ void TextInputModal::update() { Document::update(); } +MultiTextInputModal::MultiTextInputModal(Props props) : Modal(props) {} + +void MultiTextInputModal::add_input_text(const Rml::String& label, const Rml::String& startValue) { + auto modalBody = mDialog->QuerySelector(".modal-body"); + + bool isFirst = mInputs.empty(); + + mInputs.push_back(InputEntry {}); + auto& entry = mInputs.back(); + int idx = mInputs.size() - 1; + + entry.text = startValue; + entry.button = std::make_unique(modalBody, StringButton::Props{ + .key = label, + .getValue = [idx, this]{return mInputs[idx].text;}, + .setValue = [idx, this](Rml::String value) { mInputs[idx].text = value; }, + }); + + if (isFirst) + entry.button->start_editing(); +} + +bool MultiTextInputModal::handle_nav_command(Rml::Event& event, NavCommand cmd) { + auto retVal = Modal::handle_nav_command(event, cmd); + if (!retVal) { + if (cmd == NavCommand::Down) { + if (mTextSelIdx < mInputs.size()) { + mTextSelIdx++; + if (mTextSelIdx >= mInputs.size()) { + mButtons[0]->focus(); + }else { + mInputs[mTextSelIdx].button->focus(); + } + }else { + mTextSelIdx = 0; + mInputs[mTextSelIdx].button->focus(); + } + + mDoAud_seStartMenu(kSoundItemFocus); + retVal = true; + }else if (cmd == NavCommand::Up && mTextSelIdx >= 0) { + mTextSelIdx--; + if (mTextSelIdx < 0) { + mTextSelIdx = mInputs.size(); + mButtons[0]->focus(); + }else { + mInputs[mTextSelIdx].button->focus(); + } + + mDoAud_seStartMenu(kSoundItemFocus); + retVal = true; + } + } + + return retVal; +} + +void MultiTextInputModal::update() { + for (const auto& input : mInputs) { + input.button->update(); + } + Modal::update(); +} } // namespace dusk::ui diff --git a/src/dusk/ui/modal.hpp b/src/dusk/ui/modal.hpp index 2a9f31e6ac..600e77bf21 100644 --- a/src/dusk/ui/modal.hpp +++ b/src/dusk/ui/modal.hpp @@ -53,4 +53,27 @@ private: Rml::String mInputText{}; }; +class MultiTextInputModal : public Modal { +public: + explicit MultiTextInputModal(Props props); + + void add_input_text(const Rml::String& label, const Rml::String& startValue = ""); + + Rml::String get_input_text(int idx) {return mInputs[idx].text; } + + void update() override; + +protected: + bool handle_nav_command(Rml::Event& event, NavCommand cmd) override; +private: + struct InputEntry { + std::unique_ptr button; + Rml::String text; + }; + + std::vector mInputs{}; + int mTextSelIdx = 0; +}; + + } // namespace dusk::ui