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:
CraftyBoss
2026-06-22 03:25:01 -07:00
parent 93479100fc
commit 6984ae83bf
10 changed files with 424 additions and 39 deletions
+2
View File
@@ -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
+4
View File
@@ -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
View File
@@ -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() {
+34 -10
View File
@@ -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
+7 -1
View File
@@ -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);
+1 -1
View File
@@ -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)
+166
View File
@@ -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();
}
}
}
+29
View File
@@ -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);
}
+63
View File
@@ -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
+23
View File
@@ -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