mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-08-20 13:24:40 -04:00
add UI to selecting/creating archipelago seed
Next step will be to associate connection info to each save slot instead of overwriting the same config values for every slot. With these changes though, it should be possible to connect to an archipelago lobby without needing to use the ImGui debug window.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+95
-27
@@ -24,6 +24,7 @@
|
||||
#include <cstring>
|
||||
|
||||
#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>(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() {
|
||||
|
||||
@@ -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<int64_t> locations;
|
||||
// TEMP: apworld has 475 locations with ids in sequential order, so add them all individually to location set
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
#include <dusk/ui/archi_connect_modal.hpp>
|
||||
#include <dusk/ui/string_button.hpp>
|
||||
|
||||
#include <thread>
|
||||
|
||||
#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<ArchiConnectModal>());
|
||||
|
||||
if (auto* doc = top_document()) {
|
||||
doc->focus();
|
||||
}
|
||||
}
|
||||
|
||||
void CreateSetupConnectionInfoModal() {
|
||||
auto& doc = push_document(std::make_unique<MultiTextInputModal>(Modal::Props{
|
||||
.title = "Connection Info",
|
||||
.bodyRml = "",
|
||||
.actions = {
|
||||
ModalAction{
|
||||
.label = "Connect",
|
||||
.onPressed = [](Modal& modal) {
|
||||
auto textModal = dynamic_cast<MultiTextInputModal*>(&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<MultiTextInputModal*>(&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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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<StringButton>(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
|
||||
|
||||
@@ -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<StringButton> button;
|
||||
Rml::String text;
|
||||
};
|
||||
|
||||
std::vector<InputEntry> mInputs{};
|
||||
int mTextSelIdx = 0;
|
||||
};
|
||||
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
Reference in New Issue
Block a user