mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-07-07 12:03:24 -04:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 589c6675fe | |||
| 0be48db605 | |||
| dbb9ce2c03 | |||
| cc808ab50c | |||
| 7682165478 | |||
| 57019bea30 | |||
| c590018059 | |||
| e7eefbb0cb | |||
| 7ee39edee7 | |||
| 5a1368b355 | |||
| 6984ae83bf | |||
| 93479100fc | |||
| 6366ab61d1 | |||
| 7537f1c4a8 | |||
| 59b76f7aae | |||
| 4466442cfb | |||
| d6f741947a | |||
| 51a25206c2 | |||
| f6b86d3f38 | |||
| b118fa19e1 | |||
| efbf82d4c7 | |||
| 83adafff82 |
@@ -1466,6 +1466,7 @@ set(DUSK_FILES
|
||||
src/dusk/imgui/ImGuiMenuTools.hpp
|
||||
src/dusk/imgui/ImGuiMenuRandomizer.cpp
|
||||
src/dusk/imgui/ImGuiMenuRandomizer.hpp
|
||||
src/dusk/imgui/ImGuiArchipelagoDebug.cpp
|
||||
src/dusk/imgui/ImGuiActorSpawner.cpp
|
||||
src/dusk/imgui/ImGuiProcessOverlay.cpp
|
||||
src/dusk/imgui/ImGuiCameraOverlay.cpp
|
||||
@@ -1541,6 +1542,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
|
||||
@@ -1646,6 +1649,8 @@ set(DUSK_FILES
|
||||
src/dusk/randomizer/generator/utility/time.cpp
|
||||
src/dusk/randomizer/generator/utility/time.hpp
|
||||
src/dusk/randomizer/generator/utility/yaml.hpp
|
||||
# Archipelago Files
|
||||
src/dusk/archipelago/archipelago_context.cpp
|
||||
)
|
||||
|
||||
set(DUSK_HTTP_BACKEND_FILES
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -250,6 +250,7 @@ void item_func_FUSED_SHADOW_1();
|
||||
void item_func_FUSED_SHADOW_2();
|
||||
void item_func_FUSED_SHADOW_3();
|
||||
void item_func_MIRROR_PIECE_1();
|
||||
void item_func_ARCHIPELAGO_ITEM();
|
||||
#endif
|
||||
void item_func_POU_SPIRIT();
|
||||
#if TARGET_PC
|
||||
|
||||
@@ -638,7 +638,7 @@ enum {
|
||||
/* 0xD9 */ dItemNo_Randomizer_FUSED_SHADOW_2_e,
|
||||
/* 0xDA */ dItemNo_Randomizer_FUSED_SHADOW_3_e,
|
||||
/* 0xDB */ dItemNo_Randomizer_MIRROR_PIECE_1_e,
|
||||
/* 0xDC */ dItemNo_Randomizer_NOENTRY_220_e,
|
||||
/* 0xDC */ dItemNo_Randomizer_ARCHIPELAGO_ITEM_e,
|
||||
/* 0xDD */ dItemNo_Randomizer_NOENTRY_221_e,
|
||||
/* 0xDE */ dItemNo_Randomizer_NOENTRY_222_e,
|
||||
/* 0xDF */ dItemNo_Randomizer_NOENTRY_223_e,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -310,6 +310,13 @@ struct UserSettings {
|
||||
struct {
|
||||
std::array<ConfigVar<std::string>, 3> seedHashes;
|
||||
} randomizer;
|
||||
|
||||
// Archipelago Settings
|
||||
struct {
|
||||
std::array<ConfigVar<std::string>, 3> savesServerIP;
|
||||
std::array<ConfigVar<std::string>, 3> savesServerPass;
|
||||
std::array<ConfigVar<std::string>, 3> savesSlotName;
|
||||
} archipelago;
|
||||
|
||||
// Cosmetics
|
||||
struct {
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
#include "d/actor/d_a_npc_tkc.h"
|
||||
#include <cstring>
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
@@ -4311,6 +4322,10 @@ int daAlink_c::procGanonFinishInit() {
|
||||
dusk::m_speedrunInfo.stopRun();
|
||||
}
|
||||
}
|
||||
|
||||
if (dusk::archi::ArchipelagoContext::IsConnected()) {
|
||||
dusk::archi::ArchipelagoContext::TryHandleGameComplete();
|
||||
}
|
||||
#endif
|
||||
|
||||
return 1;
|
||||
|
||||
@@ -2100,6 +2100,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
|
||||
@@ -2114,7 +2149,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+106
-16
@@ -23,6 +23,9 @@
|
||||
#include "m_Do/m_Do_graphic.h"
|
||||
#include <cstring>
|
||||
|
||||
#include "dusk/archipelago/archipelago_context.hpp"
|
||||
#include "dusk/ui/archi_connect_modal.hpp"
|
||||
|
||||
#if TARGET_PC
|
||||
#include "dusk/config.hpp"
|
||||
#include "dusk/menu_pointer.h"
|
||||
@@ -299,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,
|
||||
@@ -1030,29 +1034,75 @@ void dFile_select_c::dataSelectStart() {
|
||||
ketteiTxtDispAnmInit(0);
|
||||
#endif
|
||||
|
||||
#if TARGET_PC
|
||||
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
|
||||
// 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);
|
||||
}
|
||||
// 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);
|
||||
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;
|
||||
}
|
||||
#endif
|
||||
#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);
|
||||
|
||||
mDataSelProc = DATASELPROC_SELECT_DATA_OPEN_MOVE;
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
modoruTxtDispAnmInit(1);
|
||||
@@ -1345,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
|
||||
@@ -1376,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{
|
||||
@@ -1401,6 +1447,18 @@ void dFile_select_c::selectDataPlayTypeMove() {
|
||||
modal.hide(true);
|
||||
dusk::ui::push_document(std::make_unique<dusk::ui::FileSelectRandomizerWindow>(this));
|
||||
}},
|
||||
// If Archipelago is selected, open archipelago settings window
|
||||
dusk::ui::ModalAction{
|
||||
.label = "Archipelago",
|
||||
.onPressed = [this](dusk::ui::Modal& modal) {
|
||||
// pre-emptively set file num here for context to use
|
||||
dComIfGs_setDataNum(mSelectNum);
|
||||
|
||||
mDusk.mBackToFileSelect = false;
|
||||
mDoAud_seStartMenu(Z2SE_SY_CURSOR_OK);
|
||||
modal.hide(true);
|
||||
dusk::ui::BeginArchipelagoConnectionUI(true);
|
||||
}},
|
||||
},
|
||||
// If we dismiss this modal, go back to file selection
|
||||
.onDismiss = [this](dusk::ui::Modal& modal) {
|
||||
@@ -1426,6 +1484,38 @@ void dFile_select_c::selectDataPlayTypeMove() {
|
||||
playTypeModal.focus();
|
||||
}
|
||||
}
|
||||
|
||||
void dFile_select_c::menuArchipelagoConnect() {
|
||||
if (mDusk.mArchipelagoBeginConnect) {
|
||||
// set file num here for context to use
|
||||
dComIfGs_setDataNum(mSelectNum);
|
||||
|
||||
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() {
|
||||
|
||||
+9
-1
@@ -16,6 +16,8 @@
|
||||
#endif
|
||||
#include <cstring>
|
||||
|
||||
#include "dusk/archipelago/archipelago_context.hpp"
|
||||
|
||||
static void (*item_func_ptr[256])() = {
|
||||
item_func_HEART,
|
||||
item_func_GREEN_RUPEE,
|
||||
@@ -497,7 +499,7 @@ static void (*item_func_ptr_randomizer[256])() = {
|
||||
/* 0xD9 */ item_func_FUSED_SHADOW_2,
|
||||
/* 0xDA */ item_func_FUSED_SHADOW_3,
|
||||
/* 0xDB */ item_func_MIRROR_PIECE_1,
|
||||
/* 0xDC */ item_func_noentry,
|
||||
/* 0xDC */ item_func_ARCHIPELAGO_ITEM,
|
||||
/* 0xDD */ item_func_noentry,
|
||||
/* 0xDE */ item_func_noentry,
|
||||
/* 0xDF */ item_func_noentry,
|
||||
@@ -551,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
|
||||
@@ -2180,6 +2183,11 @@ void item_func_MIRROR_PIECE_1() {
|
||||
dComIfGs_onCollectMirror(0);
|
||||
}
|
||||
}
|
||||
|
||||
void item_func_ARCHIPELAGO_ITEM() {
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
void item_func_POU_SPIRIT() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -0,0 +1,965 @@
|
||||
#include <dusk/archipelago/archipelago_context.hpp>
|
||||
|
||||
#include <thread>
|
||||
|
||||
#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"
|
||||
#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
|
||||
{
|
||||
|
||||
static constexpr int ARCHI_ITEM_OFFSET = 2320000;
|
||||
|
||||
struct SettingsNameConvert {
|
||||
static constexpr std::string kDefaultYes = "On";
|
||||
static constexpr std::string kDefaultNo = "Off";
|
||||
|
||||
std::string apName;
|
||||
std::string dusklightName;
|
||||
std::vector<std::pair<std::string, std::string>> optionsConvert;
|
||||
|
||||
const std::string& tryGetOptionConvert(const std::string& option) const {
|
||||
if (optionsConvert.empty()) {
|
||||
if (option == "Yes")
|
||||
return kDefaultYes;
|
||||
if (option == "No")
|
||||
return kDefaultNo;
|
||||
return option;
|
||||
}
|
||||
|
||||
for (const auto& value : optionsConvert) {
|
||||
if (value.first == option) {
|
||||
return value.second;
|
||||
}
|
||||
}
|
||||
return option;
|
||||
}
|
||||
};
|
||||
|
||||
static auto sArchiSettingToDusklight = std::to_array<SettingsNameConvert>({
|
||||
{"", ""},
|
||||
{"Golden Bugs Shuffled", "Golden Bugs"},
|
||||
{"Sky Chracters Shuffled", "Sky Characters"},
|
||||
{"NPC Items Shuffled", "Gifts From NPCs"},
|
||||
{"Shop Items Shuffled", "Shop Items"},
|
||||
{"Hidden Skills Shuffled", "Hidden Skills"},
|
||||
{"Skip Prologue", "Skip Prologue"},
|
||||
{"Faron Twilight Cleared", "Faron Twilight Cleared"},
|
||||
{"Eldin Twilight Cleared", "Eldin Twilight Cleared"},
|
||||
{"Lanayru Twilight Cleared", "Lanayru Twilight Cleared"},
|
||||
{"Skip MDH", "Skip Midna's Desparate Hour"},
|
||||
{"Open Map", "Unlock Map Regions"},
|
||||
{"Increase Wallet", "Logic Increase Wallet Capacity"},
|
||||
{"Transform Anywhere", "Logic Transform Anywhere"},
|
||||
{"Bonks do Damage", "Bonks Do Damage"},
|
||||
{"Lakebed Entrance Requirements", "Lakebed Does Not Require Water Bombs"},
|
||||
{"Arbiters Grounds Entrance Requirements", "Arbiters Does Not Require Bulblin Camp"},
|
||||
{"Snowpeak Entrance Requirements", "Snowpeak Does Not Require Reekfish Scent"},
|
||||
{"City in the Sky Entrance Requirements", "City Does Not Require Filled Skybook"},
|
||||
{"Goron Mines Entrance Requirements", "Goron Mines Entrance"},
|
||||
{"Palace of Twilight Requirements", "Palace of Twilight Requirements"},
|
||||
{"Faron Woods Logic", "Faron Woods Logic"},
|
||||
{"Starting ToD", "Starting Time of Day"},
|
||||
{"Skip Major Cutscenes", "Skip Major Cutscenes"},
|
||||
{"Skip Minor Cutscenes", "Skip Minor Cutscenes"},
|
||||
{"Open Door of Time", "Open Door of Time"},
|
||||
|
||||
{"Dungeon Rewards Progression", "Dungeon Rewards Can Be Anywhere", {
|
||||
// these two are functionally identical in terms of tracker logic, so treat it as such
|
||||
{"Anything", "On"},
|
||||
{"Any Progressive", "On"},
|
||||
{"Vanilla", "Off"},
|
||||
}},
|
||||
{"Small Key Settings", "Small Keys", {
|
||||
{"Startwith", "Keysy"},
|
||||
}},
|
||||
{"Big Key Settings", "Big Keys", {
|
||||
{"Startwith", "Keysy"},
|
||||
}},
|
||||
{"Map and Compass Settings", "Maps and Compasses", {
|
||||
{"Startwith", "Start With"},
|
||||
}},
|
||||
{"Trap Frequency", "Trap Item Frequency", {
|
||||
{"No Traps", "None"},
|
||||
}},
|
||||
{"Damage Magnification", "Logic Damage Multiplier", {
|
||||
{"Ohko", "OHKO"},
|
||||
}},
|
||||
{"Logic Settings", "Logic Rules", {
|
||||
{"Glitchless", "All Locations Reachable"},
|
||||
{"Glitched", "Beatable Only"},
|
||||
}},
|
||||
{"Poes Shuffled", "Poe Souls", {
|
||||
{"Yes", "All"},
|
||||
{"No", "Vanilla"}
|
||||
}}
|
||||
});
|
||||
|
||||
ArchipelagoContext& instance() {
|
||||
static ArchipelagoContext instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
const SettingsNameConvert& GetAPSettingNameConvert(const std::string& apSettingName) {
|
||||
for (const auto& entry : sArchiSettingToDusklight) {
|
||||
if (entry.apName == apSettingName)
|
||||
return entry;
|
||||
}
|
||||
return sArchiSettingToDusklight[0];
|
||||
}
|
||||
|
||||
const char* getMessageTypeName(AP_MessageType type) {
|
||||
switch (type) {
|
||||
case AP_MessageType::Plaintext:
|
||||
return "Plaintext";
|
||||
case AP_MessageType::ItemSend:
|
||||
return "ItemSend";
|
||||
case AP_MessageType::ItemRecv:
|
||||
return "ItemRecv";
|
||||
case AP_MessageType::Hint:
|
||||
return "Hint";
|
||||
case AP_MessageType::Countdown:
|
||||
return "Countdown";
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
ui::push_toast({
|
||||
.title = "Item Received",
|
||||
.content = fmt::format("Got {} From {}", recvMsg->item, recvMsg->sendPlayer),
|
||||
.duration = std::chrono::seconds(3),
|
||||
});
|
||||
// fallthrough for debug logging text contents
|
||||
}
|
||||
case AP_MessageType::Plaintext:
|
||||
case AP_MessageType::Hint:
|
||||
case AP_MessageType::Countdown:
|
||||
DuskLog.info("[{}] {}", getMessageTypeName(msg->type), msg->text);
|
||||
break;
|
||||
default:
|
||||
DuskLog.warn("Unknown message type! Type: {}", fmt::underlying(msg->type));
|
||||
break;
|
||||
}
|
||||
|
||||
AP_ClearLatestMessage();
|
||||
}
|
||||
|
||||
void ArchipelagoContext::LoadTempItemInfo() {
|
||||
auto itemDataTree = LOAD_EMBED_YAML(RANDO_DATA_PATH "items.yaml");
|
||||
for (const auto& itemNode : itemDataTree) {
|
||||
if (!itemNode["APItemId"]) {
|
||||
DuskLog.warn("Item {} missing APItemId field!", itemNode["Name"].as<std::string>());
|
||||
continue;
|
||||
}
|
||||
auto apItemId = itemNode["APItemId"].as<int>();
|
||||
|
||||
if (apItemId == -1)
|
||||
continue;
|
||||
|
||||
auto id = itemNode["Id"].as<int>();
|
||||
auto importance = randomizer::logic::item::ImportanceFromStr(itemNode["Importance"].as<std::string>());
|
||||
auto itemName = itemNode["Name"].as<std::string>();
|
||||
|
||||
m_apItemToGameItem[apItemId] = {
|
||||
id,
|
||||
importance,
|
||||
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() {
|
||||
auto locDataTree = LOAD_EMBED_YAML(RANDO_DATA_PATH "locations.yaml");
|
||||
for (const auto& locNode : locDataTree) {
|
||||
const auto& metadata = locNode["Metadata"];
|
||||
auto locationName = locNode["Name"].as<std::string>();
|
||||
|
||||
if (!metadata.IsMap()) {
|
||||
DuskLog.warn("Location {} missing correct Metadata field!", locationName);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!metadata["APLocationId"]) {
|
||||
DuskLog.warn("Location {} missing APLocationId field!", locationName);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto apLocationId = metadata["APLocationId"].as<int>();
|
||||
|
||||
if (apLocationId == -1)
|
||||
continue;
|
||||
|
||||
m_apLocToGameLoc.push_back({
|
||||
apLocationId,
|
||||
locationName
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void ArchipelagoContext::itemRecvImpl(int id, bool notify) {
|
||||
if (!m_apItemToGameItem.contains(id)) {
|
||||
DuskLog.warn("[AP] Got an invalid Item Id: {}", id);
|
||||
return;
|
||||
}
|
||||
|
||||
m_isAllowUpdateLocations = true; // guards against triggering UpdateCheckedLocations
|
||||
|
||||
auto& item = m_apItemToGameItem[id];
|
||||
|
||||
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) {
|
||||
if (!m_apItemToGameItem.contains(apId)) {
|
||||
DuskLog.warn("Got an invalid Item Id: {}", apId);
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto& item = m_apItemToGameItem[apId];
|
||||
|
||||
return item.itemId;
|
||||
}
|
||||
|
||||
std::string ArchipelagoContext::getLocationNameFromApId(int apId) const {
|
||||
for (const auto& entry : m_apLocToGameLoc) {
|
||||
if (entry.apId == apId)
|
||||
return entry.locName;
|
||||
}
|
||||
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, int file) {
|
||||
getSettings().archipelago.savesServerIP[file].setValue(std::string(ip));
|
||||
}
|
||||
|
||||
void ArchipelagoContext::SetSlotName(const std::string_view& name, int file) {
|
||||
getSettings().archipelago.savesSlotName[file].setValue(std::string(name));
|
||||
}
|
||||
|
||||
void ArchipelagoContext::SetPassword(const std::string_view& pass, int file) {
|
||||
getSettings().archipelago.savesServerPass[file].setValue(std::string(pass));
|
||||
}
|
||||
|
||||
const std::string& ArchipelagoContext::GetServerIp(int file) {
|
||||
return getSettings().archipelago.savesServerIP[file].getValue();
|
||||
}
|
||||
|
||||
const std::string& ArchipelagoContext::GetSlotName(int file) {
|
||||
return getSettings().archipelago.savesSlotName[file].getValue();
|
||||
}
|
||||
|
||||
const std::string& ArchipelagoContext::GetPassword(int file) {
|
||||
return getSettings().archipelago.savesServerPass[file].getValue();
|
||||
}
|
||||
|
||||
std::string ArchipelagoContext::GetArchipelagoSeedName() {
|
||||
if (IsConnected()) {
|
||||
auto& roomInfo = instance().m_roomInfo;
|
||||
if (roomInfo.seed_name.empty()) {
|
||||
DuskLog.warn("Got an invalid Seed Name!");
|
||||
}
|
||||
return fmt::format("AP_{}", 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();
|
||||
}
|
||||
}
|
||||
|
||||
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(int file, bool isBlocking) {
|
||||
config::Save();
|
||||
|
||||
instance().LoadTempItemInfo();
|
||||
|
||||
instance().LoadTempLocationInfo();
|
||||
|
||||
AP_SetLogCallback([](const std::string& msg) {
|
||||
DuskLog.info("{}", msg);
|
||||
});
|
||||
|
||||
AP_Init(GetServerIp(file).c_str(), "Twilight Princess", GetSlotName(file).c_str(), GetPassword(file).c_str());
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
AP_SetItemRecvCallback([](AP_NetworkItem& item, bool notify) {
|
||||
DuskLog.debug("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<AP_NetworkItem> items) {
|
||||
DuskLog.info("Got {} Location Scouts from Server.", items.size());
|
||||
HandleReceiveLocationScout(items);
|
||||
});
|
||||
|
||||
AP_RegisterSlotDataRawCallback("Settings", [](std::string data) {
|
||||
DuskLog.info("Got Settings from Slot Data.");
|
||||
instance().m_SettingsFile = data;
|
||||
});
|
||||
|
||||
AP_RegisterSlotDataRawCallback("World Version", [](std::string data) {
|
||||
DuskLog.info("TP APWorld Version: {}", data);
|
||||
});
|
||||
|
||||
AP_Start();
|
||||
|
||||
// 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() {
|
||||
AP_Shutdown();
|
||||
}
|
||||
|
||||
bool ArchipelagoContext::IsConnected() {
|
||||
auto status = AP_GetConnectionStatus();
|
||||
return status == AP_ConnectionStatus::Connected || status == AP_ConnectionStatus::Authenticated;
|
||||
}
|
||||
|
||||
void ArchipelagoContext::MessageThreadFunc() {
|
||||
DuskLog.info("AP Thread started.");
|
||||
|
||||
if (IsConnected()) {
|
||||
AP_GetRoomInfo(&instance().m_roomInfo);
|
||||
instance().m_isEnableDeathLink = AP_IsDeathLinkEnabled();
|
||||
RequestAllLocationScout();
|
||||
}
|
||||
|
||||
while (IsConnected()) {
|
||||
if (AP_IsMessagePending())
|
||||
ParseMessageData();
|
||||
}
|
||||
|
||||
DuskLog.info("AP Thread ended.");
|
||||
}
|
||||
|
||||
void ArchipelagoContext::Execute() {
|
||||
if (!IsConnected())
|
||||
return;
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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()) {
|
||||
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;
|
||||
|
||||
// TODO: modify this to also include junk items like ammo
|
||||
if (!notify && ((relativeId >= 0 && relativeId <= 6) || relativeId == 7)) {
|
||||
// skip rupee refills so players cant abuse disconnect/reconnect
|
||||
return;
|
||||
}
|
||||
|
||||
if (!instance().m_isNeedResetInv && netItem.location != -1 && IsLocationChecked(netItem.location)) {
|
||||
// no need to handle item if its location has already been checked
|
||||
return;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
playerInfo.getPlayerStatusA().setMaxLife(15);
|
||||
playerInfo.getPlayerStatusA().setWalletSize(WALLET);
|
||||
// dont reset rupees, and instead reject rupee updates while refilling inv
|
||||
|
||||
// add back default items
|
||||
|
||||
execItemGet(dItemNo_WEAR_KOKIRI_e);
|
||||
|
||||
// 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<AP_NetworkItem>& items) {
|
||||
for (const auto& item : items) {
|
||||
int parsedItemId;
|
||||
std::string parsedItemName;
|
||||
if (item.player == AP_GetPlayerID()) {
|
||||
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;
|
||||
|
||||
auto locName = instance().getLocationNameFromApId(locationId);
|
||||
|
||||
if (locName.empty()) {
|
||||
DuskLog.info("No location with ID {} found.", locationId);
|
||||
continue;
|
||||
}
|
||||
|
||||
bool collected = false;
|
||||
if (instance().m_initLocationCollectState.contains(item.location))
|
||||
collected = instance().m_initLocationCollectState[item.location];
|
||||
|
||||
instance().m_locationItemInfo[locName] = {
|
||||
parsedItemId,
|
||||
parsedItemName,
|
||||
locName,
|
||||
item.location,
|
||||
collected
|
||||
};
|
||||
}
|
||||
}
|
||||
// TODO: atm this is a sort of lazy solution to not having direct access to what location was checked when an execItemGet is called
|
||||
// so eventually finding a way to properly associate locations with their respective item get funcs would benefit this system
|
||||
void ArchipelagoContext::UpdateCheckedLocations() {
|
||||
auto& world = instance().m_archiWorld;
|
||||
|
||||
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)) {
|
||||
DuskLog.debug("No item found for ({}).", locName);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto& cachedLocData = instance().m_locationItemInfo[locName];
|
||||
|
||||
bool isCollected = isLocationObtained(location);
|
||||
|
||||
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 (!IsReceivedLocationScouts()) {
|
||||
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;
|
||||
// TODO: find out why some locations seem to keep their collection state upon reset (bugs)
|
||||
|
||||
for (const auto& [locName, locInfo] : instance().m_locationItemInfo) {
|
||||
auto location = world->GetLocation(locInfo.locationName, true);
|
||||
if (!location || !location->IsProgression())
|
||||
continue;
|
||||
|
||||
setLocationCollected(location, locInfo.collected);
|
||||
}
|
||||
}
|
||||
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
||||
bool ArchipelagoContext::TryHandleGameComplete() {
|
||||
// TODO: maybe add support for other game completion types?
|
||||
AP_StoryComplete();
|
||||
return true;
|
||||
}
|
||||
|
||||
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
|
||||
// (eventually we will iterate through locations.yaml for a better data-driven solution)
|
||||
for (int i = 0; i < 475; i++) {
|
||||
locations.insert(ARCHI_ITEM_OFFSET + i);
|
||||
}
|
||||
|
||||
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 {
|
||||
apConfigYaml = YAML::Load(settingsStr);
|
||||
}catch (YAML::BadFile& e) {
|
||||
DuskLog.warn("Failed to load AP Config YAML file!");
|
||||
return false;
|
||||
}
|
||||
|
||||
config.SetSeed("Archipelago");
|
||||
randomizer::seedgen::settings::Settings& settings = config.GetSettings();
|
||||
|
||||
// update settings using ap config
|
||||
for (const auto& apSettingEntry : apConfigYaml) {
|
||||
auto apSettingName = apSettingEntry.first.as<std::string>();
|
||||
auto apSettingValue = apSettingEntry.second.as<std::string>();
|
||||
|
||||
const auto& settingConvert = GetAPSettingNameConvert(apSettingName);
|
||||
|
||||
if (!settingConvert.apName.empty()) {
|
||||
auto& setting = settings.GetMap().at(settingConvert.dusklightName);
|
||||
setting.SetCurrentOption(settingConvert.tryGetOptionConvert(apSettingValue));
|
||||
} else 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 == "Temple of Time Entrance Requirements") {
|
||||
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 {
|
||||
DuskLog.debug("Missing Setting: {} Value: {}", apSettingName, apSettingValue);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int ArchipelagoContext::GetItemAtLocation(const std::string& locName) {
|
||||
if (!instance().m_locationItemInfo.contains(locName)) {
|
||||
DuskLog.warn("No item found for ({}).", locName);
|
||||
return 0;
|
||||
}
|
||||
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");
|
||||
|
||||
if (instance().m_SettingsFile.empty()) {
|
||||
DuskLog.fatal("Settings Data was not sent to client! Unable to generate world data.");
|
||||
return;
|
||||
}
|
||||
|
||||
GenerateConfigFromAP(instance().m_config, instance().m_SettingsFile);
|
||||
|
||||
instance().m_config.WriteToFile(workingDir / "settings.yaml", workingDir / "preferences.yaml");
|
||||
|
||||
createContext = true;
|
||||
}
|
||||
|
||||
CreateArchipelagoWorld();
|
||||
|
||||
FillArchipelagoWorld();
|
||||
|
||||
if (createContext) {
|
||||
CreateRandomizerContext();
|
||||
}else {
|
||||
LoadRandomizerContext();
|
||||
}
|
||||
}
|
||||
} // dusk::archi
|
||||
@@ -0,0 +1,149 @@
|
||||
#pragma once
|
||||
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
#include "Archipelago.h"
|
||||
|
||||
namespace dusk::archi
|
||||
{
|
||||
class ArchipelagoContext {
|
||||
private:
|
||||
struct TEMP_GameItemInfo {
|
||||
int itemId = -1;
|
||||
randomizer::logic::item::Importance importance = randomizer::logic::item::Importance::INVALID;
|
||||
std::string itemName;
|
||||
};
|
||||
|
||||
struct TEMP_GameLocationInfo {
|
||||
int apId = -1;
|
||||
std::string locName;
|
||||
};
|
||||
|
||||
struct GameLocationInfo {
|
||||
int itemId = -1;
|
||||
std::string itemName;
|
||||
std::string locationName;
|
||||
int64_t apLocationId = -1;
|
||||
bool collected = false;
|
||||
};
|
||||
|
||||
std::vector<std::pair<int, bool>> m_receivedItemsQueue;
|
||||
std::mutex m_queueMutex;
|
||||
|
||||
// Rando Data
|
||||
randomizer::seedgen::config::Config m_config;
|
||||
std::unique_ptr<randomizer::logic::world::World> m_archiWorld = nullptr;
|
||||
bool m_isUpdateLocations = false;
|
||||
bool m_isNeedResetInv = false;
|
||||
bool m_isAllowUpdateLocations = false;
|
||||
bool m_isEnableDeathLink = false;
|
||||
|
||||
// AP Data
|
||||
std::unordered_map<std::string, GameLocationInfo> m_locationItemInfo;
|
||||
std::map<int, bool> m_initLocationCollectState;
|
||||
AP_RoomInfo m_roomInfo;
|
||||
std::string m_SettingsFile;
|
||||
bool m_isNeedPlayerDeath = false;
|
||||
bool m_isFromDeathLink = false;
|
||||
|
||||
// TEMP
|
||||
std::map<int, TEMP_GameItemInfo> m_apItemToGameItem;
|
||||
std::vector<TEMP_GameLocationInfo> m_apLocToGameLoc;
|
||||
|
||||
void LoadTempItemInfo();
|
||||
|
||||
void LoadTempLocationInfo();
|
||||
|
||||
void itemRecvImpl(int id, bool notify);
|
||||
|
||||
int getItemIdFromApId(int apId);
|
||||
|
||||
std::string getLocationNameFromApId(int apId) const;
|
||||
|
||||
bool tryKillPlayer();
|
||||
public:
|
||||
ArchipelagoContext();
|
||||
|
||||
// Config Getters/Setters
|
||||
|
||||
static void SetServerIp(const std::string_view& ip, int file);
|
||||
static void SetSlotName(const std::string_view& name, int file);
|
||||
static void SetPassword(const std::string_view& pass, int file);
|
||||
|
||||
static const std::string& GetServerIp(int file);
|
||||
static const std::string& GetSlotName(int file);
|
||||
static const std::string& GetPassword(int file);
|
||||
|
||||
static std::string GetArchipelagoSeedName();
|
||||
|
||||
static void GetSeedDirectoryPath(std::filesystem::path& outPath);
|
||||
|
||||
static bool IsSeedHashArchipelago(const std::string& seedStr);
|
||||
|
||||
static bool IsCurrentSeedHash(const std::string& seedStr);
|
||||
|
||||
// Connection Handlers
|
||||
|
||||
static bool ConnectToServer(int file, bool isBlocking = false);
|
||||
|
||||
static void DisconnectFromServer();
|
||||
|
||||
static bool IsConnected();
|
||||
|
||||
// State Handlers
|
||||
|
||||
static void MessageThreadFunc();
|
||||
|
||||
static void Execute();
|
||||
|
||||
static void HandleItemReceived(AP_NetworkItem& id, bool notify);
|
||||
|
||||
static void HandleResetInventory();
|
||||
|
||||
static void HandleReceiveLocationScout(const std::vector<AP_NetworkItem>& items);
|
||||
|
||||
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();
|
||||
|
||||
static bool IsReceivedLocationScouts();
|
||||
|
||||
static void TryHandleDeathLink();
|
||||
|
||||
static bool TryHandleGameComplete();
|
||||
|
||||
// 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);
|
||||
|
||||
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
|
||||
@@ -0,0 +1,60 @@
|
||||
#include <dusk/imgui/ImGuiArchipelagoDebug.h>
|
||||
|
||||
#include "imgui.h"
|
||||
|
||||
#include "Archipelago.h"
|
||||
#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 {
|
||||
|
||||
ImGuiArchipelagoDebug::ImGuiArchipelagoDebug() {
|
||||
|
||||
}
|
||||
|
||||
void ImGuiArchipelagoDebug::drawMenuItem() {
|
||||
if (ImGui::BeginMenu("Randomizer")) {
|
||||
ImGui::Checkbox("Archipelago Window", &m_drawWindow);
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
}
|
||||
|
||||
void ImGuiArchipelagoDebug::drawWindow() {
|
||||
if (!m_drawWindow)
|
||||
return;
|
||||
|
||||
if (!m_hasInitValues) {
|
||||
m_hasInitValues = true;
|
||||
}
|
||||
|
||||
ImGui::Begin("Archipelago Debug", &m_drawWindow);
|
||||
|
||||
if (archi::ArchipelagoContext::IsConnected()) {
|
||||
if (ImGui::Button("Test Create World Data")) {
|
||||
archi::ArchipelagoContext::GenerateLocalWorldData();
|
||||
}
|
||||
|
||||
if (ImGui::Button("Disconnect")) {
|
||||
archi::ArchipelagoContext::DisconnectFromServer();
|
||||
}
|
||||
}else {
|
||||
if (ImGui::Button("Connect")) {
|
||||
archi::ArchipelagoContext::ConnectToServer(dComIfGs_getDataNum());
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
namespace dusk {
|
||||
class ImGuiArchipelagoDebug {
|
||||
private:
|
||||
bool m_drawWindow = false;
|
||||
bool m_showPassword = false;
|
||||
bool m_hasInitValues = false;
|
||||
|
||||
char m_serverIpInputBuffer[0x40] = {};
|
||||
char m_serverPassInputBuffer[0x40] = {};
|
||||
char m_slotNameInputBuffer[0x40] = {};
|
||||
public:
|
||||
ImGuiArchipelagoDebug();
|
||||
|
||||
void drawMenuItem();
|
||||
void drawWindow();
|
||||
};
|
||||
}
|
||||
@@ -278,6 +278,7 @@ namespace dusk {
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f));
|
||||
if (showMenu && ImGui::BeginMainMenuBar()) {
|
||||
m_menuRandomizer.draw();
|
||||
m_menuArchipelago.drawMenuItem();
|
||||
m_menuTools.draw();
|
||||
|
||||
ImGui::EndMainMenuBar();
|
||||
@@ -375,6 +376,7 @@ namespace dusk {
|
||||
m_menuRandomizer.windowRandoGeneration();
|
||||
m_menuRandomizer.windowRandoTracker();
|
||||
|
||||
m_menuArchipelago.drawWindow();
|
||||
}
|
||||
|
||||
void ImGuiConsole::PostDraw() {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "ImGuiMenuRandomizer.hpp"
|
||||
#include "dusk/main.h"
|
||||
#include "imgui.h"
|
||||
#include "ImGuiArchipelagoDebug.h"
|
||||
|
||||
union SDL_Event;
|
||||
struct ImGuiWindow;
|
||||
@@ -34,6 +35,7 @@ private:
|
||||
ImVec2 m_dragScrollLastMousePos = {};
|
||||
|
||||
ImGuiMenuRandomizer m_menuRandomizer;
|
||||
ImGuiArchipelagoDebug m_menuArchipelago;
|
||||
|
||||
// Keep always last
|
||||
ImGuiMenuTools m_menuTools;
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
#include <numeric>
|
||||
#include <ranges>
|
||||
|
||||
#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()) {
|
||||
|
||||
@@ -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<std::string> RandomizerContext::WriteToFile() {
|
||||
return WriteToFile(this->GetSeedDataPath());
|
||||
}
|
||||
|
||||
std::ofstream seedData(this->GetSeedDataPath());
|
||||
std::optional<std::string> RandomizerContext::WriteToFile(const fspath& path) {
|
||||
std::ofstream seedData(path);
|
||||
if (!seedData.is_open()) {
|
||||
return "Could not open seed data file";
|
||||
}
|
||||
@@ -141,7 +145,11 @@ std::optional<std::string> RandomizerContext::LoadFromHash(const std::string& ha
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto in = LoadYAML(this->GetSeedDataPath());
|
||||
return LoadFromPath(this->GetSeedDataPath());
|
||||
}
|
||||
|
||||
std::optional<std::string> RandomizerContext::LoadFromPath(const fspath& path) {
|
||||
auto in = LoadYAML(path);
|
||||
|
||||
// Necessary settings
|
||||
for (const auto& settingNode : in["mSettings"] ) {
|
||||
@@ -601,6 +609,8 @@ int RandomizerState::execute() {
|
||||
handleFoolishItem();
|
||||
}
|
||||
|
||||
dusk::archi::ArchipelagoContext::Execute();
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -1036,6 +1046,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
|
||||
|
||||
@@ -81,7 +81,9 @@ public:
|
||||
std::unordered_map<int, EntranceOverride> mEntranceOverrides{};
|
||||
|
||||
std::optional<std::string> WriteToFile();
|
||||
std::optional<std::string> WriteToFile(const fspath& path);;
|
||||
std::optional<std::string> LoadFromHash(const std::string& hash);
|
||||
std::optional<std::string> LoadFromPath(const fspath& path);
|
||||
std::filesystem::path GetSeedDataPath() const;
|
||||
|
||||
enum Settings {
|
||||
@@ -277,4 +279,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
|
||||
|
||||
@@ -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<u8>();
|
||||
auto stageId = getStageSaveId(chestNode[0]["Stage"].as<u8>());
|
||||
if (collect)
|
||||
dComIfGs_onStageTbox(stageId, tboxId);
|
||||
else
|
||||
dComIfGs_offStageTbox(stageId, tboxId);
|
||||
}
|
||||
if (auto& poeNode = locationMeta["Poe"]) {
|
||||
auto flag = poeNode[0]["Flag"].as<u8>();
|
||||
auto stageId = getStageSaveId(poeNode[0]["Stage"].as<u8>());
|
||||
if (collect)
|
||||
dComIfGs_onStageSwitch(stageId, flag);
|
||||
else
|
||||
dComIfGs_offStageSwitch(stageId, flag);
|
||||
}
|
||||
if (auto& freeStandingItemNode = locationMeta["Freestanding Item"]) {
|
||||
auto flag = freeStandingItemNode[0]["Flag"].as<u8>();
|
||||
auto stageId = getStageSaveId(freeStandingItemNode[0]["Stage"].as<u8>());
|
||||
// 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<u16>();
|
||||
if (collect)
|
||||
dComIfGs_onEventBit(flag);
|
||||
else
|
||||
dComIfGs_offEventBit(flag);
|
||||
}
|
||||
if (auto& wolfNode = locationMeta["Golden Wolf"]) {
|
||||
auto flag = wolfNode[0]["Flag"].as<u16>();
|
||||
if (collect)
|
||||
dComIfGs_onEventBit(flag);
|
||||
else
|
||||
dComIfGs_offEventBit(flag);
|
||||
}
|
||||
if (auto& switchFlagNode = locationMeta["Switch Flag"]) {
|
||||
auto flag = switchFlagNode["Flag"].as<u8>();
|
||||
auto stageId = getStageSaveId(switchFlagNode["Stage"].as<u8>());
|
||||
if (collect)
|
||||
dComIfGs_onStageSwitch(stageId, flag);
|
||||
else
|
||||
dComIfGs_offStageSwitch(stageId, flag);
|
||||
}
|
||||
if (auto& itemFlagNode = locationMeta["Item Flag"]) {
|
||||
auto flag = itemFlagNode["Flag"].as<u8>();
|
||||
auto stageId = getStageSaveId(itemFlagNode["Stage"].as<u8>());
|
||||
if (collect)
|
||||
dComIfGs_onStageItem(stageId, flag);
|
||||
else
|
||||
dComIfGs_offStageItem(stageId, flag);
|
||||
}
|
||||
if (auto& twilitInsectNode = locationMeta["Twilit Insect"]) {
|
||||
auto flag = twilitInsectNode[0]["Flag"].as<u8>();
|
||||
auto stageId = getStageSaveId(twilitInsectNode[0]["Stage"].as<u8>());
|
||||
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<u8>();
|
||||
auto stageId = getStageSaveId(chestNode[0]["Stage"].as<u8>());
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -11,30 +11,37 @@
|
||||
- Name: Green Rupee
|
||||
Importance: Junk
|
||||
Id: 0x01
|
||||
APItemId: 0
|
||||
|
||||
- Name: Blue Rupee
|
||||
Importance: Junk
|
||||
Id: 0x02
|
||||
APItemId: 1
|
||||
|
||||
- Name: Yellow Rupee
|
||||
Importance: Junk
|
||||
Id: 0x03
|
||||
APItemId: 2
|
||||
|
||||
- Name: Red Rupee
|
||||
Importance: Junk
|
||||
Id: 0x04
|
||||
APItemId: 3
|
||||
|
||||
- Name: Purple Rupee
|
||||
Importance: Junk
|
||||
Id: 0x05
|
||||
APItemId: 4
|
||||
|
||||
- Name: Orange Rupee
|
||||
Importance: Junk
|
||||
Id: 0x06
|
||||
APItemId: 5
|
||||
|
||||
- Name: Silver Rupee
|
||||
Importance: Junk
|
||||
Id: 0x07
|
||||
APItemId: 6
|
||||
|
||||
#- Name: Unused (Gives text about combining bombs)
|
||||
# Importance:
|
||||
@@ -47,30 +54,37 @@
|
||||
- Name: Bombs 5
|
||||
Importance: Junk
|
||||
Id: 0x0A
|
||||
APItemId: 8
|
||||
|
||||
- Name: Bombs 10
|
||||
Importance: Junk
|
||||
Id: 0x0B
|
||||
APItemId: 9
|
||||
|
||||
- Name: Bombs 20
|
||||
Importance: Junk
|
||||
Id: 0x0C
|
||||
APItemId: 10
|
||||
|
||||
- Name: Bombs 30
|
||||
Importance: Junk
|
||||
Id: 0x0D
|
||||
APItemId: 11
|
||||
|
||||
- Name: Arrows 10
|
||||
Importance: Junk
|
||||
Id: 0x0E
|
||||
APItemId: 12
|
||||
|
||||
- Name: Arrows 20
|
||||
Importance: Junk
|
||||
Id: 0x0F
|
||||
APItemId: 13
|
||||
|
||||
- Name: Arrows 30
|
||||
Importance: Junk
|
||||
Id: 0x10
|
||||
APItemId: 14
|
||||
|
||||
#- Name: Arrows 1
|
||||
# Importance: Junk
|
||||
@@ -79,46 +93,57 @@
|
||||
- Name: Seeds 50
|
||||
Importance: Junk
|
||||
Id: 0x12
|
||||
APItemId: 15
|
||||
|
||||
- Name: Foolish Item # Custom Rando Item
|
||||
Importance: Junk
|
||||
Id: 0x13
|
||||
APItemId: 131
|
||||
|
||||
- Name: Ordon Spring Portal # Custom Rando item
|
||||
Importance: Major
|
||||
Id: 0x14
|
||||
APItemId: -1
|
||||
|
||||
- Name: South Faron Portal # Custom Rando Item
|
||||
Importance: Major
|
||||
Id: 0x15
|
||||
APItemId: -1
|
||||
|
||||
- Name: Water Bombs 5
|
||||
Importance: Junk
|
||||
Id: 0x16
|
||||
APItemId: 17
|
||||
|
||||
- Name: Water Bombs 10
|
||||
Importance: Junk
|
||||
Id: 0x17
|
||||
APItemId: 18
|
||||
|
||||
- Name: Water Bombs 15
|
||||
Importance: Junk
|
||||
Id: 0x18
|
||||
APItemId: 19
|
||||
|
||||
#- Name: Water Bombs 3
|
||||
# Importance: Junk
|
||||
# Id: 0x19
|
||||
# APItemId: 16
|
||||
|
||||
- Name: Bomblings 5
|
||||
Importance: Junk
|
||||
Id: 0x1A
|
||||
APItemId: 21
|
||||
|
||||
- Name: Bomblings 10
|
||||
Importance: Junk
|
||||
Id: 0x1B
|
||||
APItemId: 22
|
||||
|
||||
#- Name: Bomblings 3
|
||||
# Importance: Junk
|
||||
# Id: 0x1C
|
||||
# APItemId: 20
|
||||
#
|
||||
#- Name: Bomblings 1
|
||||
# Importance: Junk
|
||||
@@ -139,10 +164,12 @@
|
||||
- Name: Piece of Heart
|
||||
Importance: Junk
|
||||
Id: 0x21
|
||||
APItemId: 23
|
||||
|
||||
- Name: Heart Container
|
||||
Importance: Junk
|
||||
Id: 0x22
|
||||
APItemId: 24
|
||||
|
||||
#- Name: Dungeon Map
|
||||
# Importance: Junk
|
||||
@@ -175,14 +202,17 @@
|
||||
- Name: Ordon Shield
|
||||
Importance: Major
|
||||
Id: 0x2A
|
||||
APItemId: 26
|
||||
|
||||
- Name: Wooden Shield
|
||||
Importance: Junk
|
||||
Id: 0x2B
|
||||
APItemId: 26
|
||||
|
||||
- Name: Hylian Shield
|
||||
Importance: Major
|
||||
Id: 0x2C
|
||||
APItemId: 27
|
||||
|
||||
#- Name: Ooccoo's Note
|
||||
# Importance: Junk
|
||||
@@ -199,14 +229,17 @@
|
||||
- Name: Magic Armor
|
||||
Importance: Major
|
||||
Id: 0x30
|
||||
APItemId: 28
|
||||
|
||||
- Name: Zora Armor
|
||||
Importance: Major
|
||||
Id: 0x31
|
||||
APItemId: 29
|
||||
|
||||
- Name: Shadow Crystal
|
||||
Importance: Major
|
||||
Id: 0x32
|
||||
APItemId: 30
|
||||
|
||||
#- Name: Ooccoo Dungeon
|
||||
# Importance: Junk
|
||||
@@ -219,6 +252,7 @@
|
||||
- Name: Progressive Wallet # Also Large Wallet
|
||||
Importance: Major
|
||||
Id: 0x35
|
||||
APItemId: 31
|
||||
|
||||
#- Name: Giant Wallet
|
||||
# Importance: Major
|
||||
@@ -235,18 +269,22 @@
|
||||
- Name: Upper Zoras River Portal
|
||||
Importance: Major
|
||||
Id: 0x39
|
||||
APItemId: -1
|
||||
|
||||
- Name: Castle Town Portal
|
||||
Importance: Major
|
||||
Id: 0x3A
|
||||
APItemId: -1
|
||||
|
||||
- Name: Gerudo Desert Portal
|
||||
Importance: Major
|
||||
Id: 0x3B
|
||||
APItemId: -1
|
||||
|
||||
- Name: North Faron Portal
|
||||
Importance: Major
|
||||
Id: 0x3C
|
||||
APItemId: -1
|
||||
|
||||
#- Name: Coral Earring
|
||||
# Importance: Major
|
||||
@@ -255,38 +293,47 @@
|
||||
- Name: Hawkeye
|
||||
Importance: Minor
|
||||
Id: 0x3E
|
||||
APItemId: 32
|
||||
|
||||
- Name: Progressive Sword # Also Wooden Sword
|
||||
Importance: Major
|
||||
Id: 0x3F
|
||||
APItemId: 25
|
||||
|
||||
- Name: Gale Boomerang
|
||||
Importance: Major
|
||||
Id: 0x40
|
||||
APItemId: 33
|
||||
|
||||
- Name: Spinner
|
||||
Importance: Major
|
||||
Id: 0x41
|
||||
APItemId: 34
|
||||
|
||||
- Name: Ball and Chain
|
||||
Importance: Major
|
||||
Id: 0x42
|
||||
APItemId: 35
|
||||
|
||||
- Name: Progressive Bow
|
||||
Importance: Major
|
||||
Id: 0x43
|
||||
APItemId: 36
|
||||
|
||||
- Name: Progressive Clawshot
|
||||
Importance: Major
|
||||
Id: 0x44
|
||||
APItemId: 37
|
||||
|
||||
- Name: Iron Boots
|
||||
Importance: Major
|
||||
Id: 0x45
|
||||
APItemId: 38
|
||||
|
||||
- Name: Progressive Dominion Rod
|
||||
Importance: Major
|
||||
Id: 0x46
|
||||
APItemId: 39
|
||||
|
||||
#- Name: Double Clawshots
|
||||
# Importance: Major
|
||||
@@ -295,6 +342,7 @@
|
||||
- Name: Lantern
|
||||
Importance: Major
|
||||
Id: 0x48
|
||||
APItemId: 40
|
||||
|
||||
#- Name: Master Sword with Light
|
||||
# Importance: Major
|
||||
@@ -303,10 +351,12 @@
|
||||
- Name: Progressive Fishing Rod
|
||||
Importance: Major
|
||||
Id: 0x4A
|
||||
APItemId: 41
|
||||
|
||||
- Name: Slingshot
|
||||
Importance: Major
|
||||
Id: 0x4B
|
||||
APItemId: 44
|
||||
|
||||
#- Name: Dominion Rod Uncharged
|
||||
# Importance: Major
|
||||
@@ -315,18 +365,22 @@
|
||||
- Name: Kakariko Gorge Portal
|
||||
Importance: Major
|
||||
Id: 0x4D
|
||||
APItemId: -1
|
||||
|
||||
- Name: Kakariko Village Portal
|
||||
Importance: Major
|
||||
Id: 0x4E
|
||||
APItemId: -1
|
||||
|
||||
- Name: Giant Bomb Bag
|
||||
Importance: Minor
|
||||
Id: 0x4F
|
||||
APItemId: 133
|
||||
|
||||
- Name: Bomb Bag
|
||||
Importance: Major
|
||||
Id: 0x50
|
||||
APItemId: 45
|
||||
|
||||
#- Name: Also Bomb Bag (different text)
|
||||
# Importance: Major
|
||||
@@ -335,6 +389,7 @@
|
||||
- Name: Death Mountain Portal
|
||||
Importance: Major
|
||||
Id: 0x52
|
||||
APItemId: -1
|
||||
|
||||
#- Name: Light Arrow
|
||||
# Importance: Major
|
||||
@@ -355,6 +410,7 @@
|
||||
- Name: Zoras Domain Portal
|
||||
Importance: Major
|
||||
Id: 0x57
|
||||
APItemId: -1
|
||||
|
||||
#- Name: Fising Rod Lure
|
||||
# Importance: Junk
|
||||
@@ -391,10 +447,12 @@
|
||||
- Name: Empty Bottle
|
||||
Importance: Major
|
||||
Id: 0x60
|
||||
APItemId: 46
|
||||
|
||||
- Name: Red Potion Shop
|
||||
Importance: Junk
|
||||
Id: 0x61
|
||||
APItemId: -1
|
||||
|
||||
#- Name: Green Potion Shop
|
||||
# Importance: Junk
|
||||
@@ -403,6 +461,7 @@
|
||||
- Name: Blue Potion Shop
|
||||
Importance: Junk
|
||||
Id: 0x63
|
||||
APItemId: -1
|
||||
|
||||
#- Name: Milk
|
||||
# Importance: Junk
|
||||
@@ -411,6 +470,7 @@
|
||||
- Name: Bottle with Half Milk
|
||||
Importance: Major
|
||||
Id: 0x65
|
||||
APItemId: 47
|
||||
|
||||
#- Name: Lantern Oil Shop
|
||||
# Importance: Junk
|
||||
@@ -467,6 +527,7 @@
|
||||
- Name: Fairy Tears
|
||||
Importance: Junk
|
||||
Id: 0x73
|
||||
APItemId: -1
|
||||
|
||||
#- Name: Worm
|
||||
# Importance: Junk
|
||||
@@ -475,6 +536,7 @@
|
||||
- Name: Bottle with Great Fairies Tears
|
||||
Importance: Major
|
||||
Id: 0x75
|
||||
APItemId: 49
|
||||
|
||||
#- Name: Bee Larva Scooped
|
||||
# Importance: Junk
|
||||
@@ -519,133 +581,161 @@
|
||||
- Name: Renados Letter
|
||||
Importance: Major
|
||||
Id: 0x80
|
||||
APItemId: -1
|
||||
|
||||
- Name: Invoice
|
||||
Importance: Major
|
||||
Id: 0x81
|
||||
APItemId: -1
|
||||
|
||||
- Name: Wooden Statue
|
||||
Importance: Major
|
||||
Id: 0x82
|
||||
APItemId: -1
|
||||
|
||||
- Name: Ilias Charm
|
||||
Importance: Major
|
||||
Id: 0x83
|
||||
APItemId: -1
|
||||
|
||||
- Name: Horse Call
|
||||
Importance: Minor
|
||||
Id: 0x84
|
||||
APItemId: 53
|
||||
|
||||
- Name: Forest Temple Small Key
|
||||
Importance: Major
|
||||
Id: 0x85
|
||||
Dungeon Small Key: Forest Temple
|
||||
APItemId: 54
|
||||
|
||||
- Name: Goron Mines Small Key
|
||||
Importance: Major
|
||||
Id: 0x86
|
||||
Dungeon Small Key: Goron Mines
|
||||
APItemId: 55
|
||||
|
||||
- Name: Lakebed Temple Small Key
|
||||
Importance: Major
|
||||
Id: 0x87
|
||||
Dungeon Small Key: Lakebed Temple
|
||||
APItemId: 56
|
||||
|
||||
- Name: Arbiters Grounds Small Key
|
||||
Importance: Major
|
||||
Id: 0x88
|
||||
Dungeon Small Key: Arbiters Grounds
|
||||
APItemId: 57
|
||||
|
||||
- Name: Snowpeak Ruins Small Key
|
||||
Importance: Major
|
||||
Id: 0x89
|
||||
Dungeon Small Key: Snowpeak Ruins
|
||||
APItemId: 58
|
||||
|
||||
- Name: Temple of Time Small Key
|
||||
Importance: Major
|
||||
Id: 0x8A
|
||||
Dungeon Small Key: Temple of Time
|
||||
APItemId: 59
|
||||
|
||||
- Name: City in the Sky Small Key
|
||||
Importance: Major
|
||||
Id: 0x8B
|
||||
Dungeon Small Key: City in the Sky
|
||||
APItemId: 60
|
||||
|
||||
- Name: Palace of Twilight Small Key
|
||||
Importance: Major
|
||||
Id: 0x8C
|
||||
Dungeon Small Key: Palace of Twilight
|
||||
APItemId: 61
|
||||
|
||||
- Name: Hyrule Castle Small Key
|
||||
Importance: Major
|
||||
Id: 0x8D
|
||||
Dungeon Small Key: Hyrule Castle
|
||||
APItemId: 62
|
||||
|
||||
- Name: Gerudo Desert Bulblin Camp Key
|
||||
Importance: Major
|
||||
Id: 0x8E
|
||||
APItemId: 66
|
||||
|
||||
- Name: Lake Hylia Portal
|
||||
Importance: Major
|
||||
Id: 0x8F
|
||||
APItemId: -1
|
||||
|
||||
- Name: Aurus Memo
|
||||
Importance: Major
|
||||
Id: 0x90
|
||||
APItemId: 67
|
||||
|
||||
- Name: Asheis Sketch
|
||||
Importance: Major
|
||||
Id: 0x91
|
||||
APItemId: 68
|
||||
|
||||
- Name: Forest Temple Big Key
|
||||
Importance: Major
|
||||
Id: 0x92
|
||||
Dungeon Big Key: Forest Temple
|
||||
APItemId: 69
|
||||
|
||||
- Name: Lakebed Temple Big Key
|
||||
Importance: Major
|
||||
Id: 0x93
|
||||
Dungeon Big Key: Lakebed Temple
|
||||
APItemId: 70
|
||||
|
||||
- Name: Arbiters Grounds Big Key
|
||||
Importance: Major
|
||||
Id: 0x94
|
||||
Dungeon Big Key: Arbiters Grounds
|
||||
APItemId: 72
|
||||
|
||||
- Name: Temple of Time Big Key
|
||||
Importance: Major
|
||||
Id: 0x95
|
||||
Dungeon Big Key: Temple of Time
|
||||
APItemId: 74
|
||||
|
||||
- Name: City in the Sky Big Key
|
||||
Importance: Major
|
||||
Id: 0x96
|
||||
Dungeon Big Key: City in the Sky
|
||||
APItemId: 75
|
||||
|
||||
- Name: Palace of Twilight Big Key
|
||||
Importance: Major
|
||||
Id: 0x97
|
||||
Dungeon Big Key: Palace of Twilight
|
||||
APItemId: 76
|
||||
|
||||
- Name: Hyrule Castle Big Key
|
||||
Importance: Major
|
||||
Id: 0x98
|
||||
Dungeon Big Key: Hyrule Castle
|
||||
APItemId: 77
|
||||
|
||||
- Name: Forest Temple Compass
|
||||
Importance: Junk
|
||||
Id: 0x99
|
||||
Dungeon Compass: Forest Temple
|
||||
APItemId: 78
|
||||
|
||||
- Name: Goron Mines Compass
|
||||
Importance: Junk
|
||||
Id: 0x9A
|
||||
Dungeon Compass: Goron Mines
|
||||
APItemId: 79
|
||||
|
||||
- Name: Lakebed Temple Compass
|
||||
Importance: Junk
|
||||
Id: 0x9B
|
||||
Dungeon Compass: Lakebed Temple
|
||||
APItemId: 80
|
||||
|
||||
#- Name: Lantern Yellow Chu Chu
|
||||
# Importance: Junk
|
||||
@@ -654,6 +744,7 @@
|
||||
- Name: Bottle with Lantern Oil
|
||||
Importance: Major
|
||||
Id: 0x9D
|
||||
APItemId: 48
|
||||
|
||||
#- Name: Bee Larva Shop
|
||||
# Importance: Junk
|
||||
@@ -686,6 +777,7 @@
|
||||
- Name: Progressive Mirror Shard
|
||||
Importance: Major
|
||||
Id: 0xA5
|
||||
APItemId: 120
|
||||
|
||||
#- Name: Mirror Piece 3
|
||||
# Importance: Junk
|
||||
@@ -699,39 +791,47 @@
|
||||
Importance: Junk
|
||||
Id: 0xA8
|
||||
Dungeon Compass: Arbiters Grounds
|
||||
APItemId: 81
|
||||
|
||||
- Name: Snowpeak Ruins Compass
|
||||
Importance: Junk
|
||||
Id: 0xA9
|
||||
Dungeon Compass: Snowpeak Ruins
|
||||
APItemId: 82
|
||||
|
||||
- Name: Temple of Time Compass
|
||||
Importance: Junk
|
||||
Id: 0xAA
|
||||
Dungeon Compass: Temple of Time
|
||||
APItemId: 83
|
||||
|
||||
- Name: City in the Sky Compass
|
||||
Importance: Junk
|
||||
Id: 0xAB
|
||||
Dungeon Compass: City in the Sky
|
||||
APItemId: 84
|
||||
|
||||
- Name: Palace of Twilight Compass
|
||||
Importance: Junk
|
||||
Id: 0xAC
|
||||
Dungeon Compass: Palace of Twilight
|
||||
APItemId: 85
|
||||
|
||||
- Name: Hyrule Castle Compass
|
||||
Importance: Junk
|
||||
Id: 0xAD
|
||||
Dungeon Compass: Hyrule Castle
|
||||
APItemId: 86
|
||||
|
||||
- Name: Mirror Chamber Portal
|
||||
Importance: Major
|
||||
Id: 0xAE
|
||||
APItemId: -1
|
||||
|
||||
- Name: Snowpeak Portal
|
||||
Importance: Major
|
||||
Id: 0xAF
|
||||
APItemId: -1
|
||||
|
||||
#- Name: Ilias Scent
|
||||
# Importance: Junk
|
||||
@@ -761,150 +861,185 @@
|
||||
Importance: Junk
|
||||
Id: 0xB6
|
||||
Dungeon Map: Forest Temple
|
||||
APItemId: 87
|
||||
|
||||
- Name: Goron Mines Dungeon Map
|
||||
Importance: Junk
|
||||
Id: 0xB7
|
||||
Dungeon Map: Goron Mines
|
||||
APItemId: 88
|
||||
|
||||
- Name: Lakebed Temple Dungeon Map
|
||||
Importance: Junk
|
||||
Id: 0xB8
|
||||
Dungeon Map: Lakebed Temple
|
||||
APItemId: 89
|
||||
|
||||
- Name: Arbiters Grounds Dungeon Map
|
||||
Importance: Junk
|
||||
Id: 0xB9
|
||||
Dungeon Map: Arbiters Grounds
|
||||
APItemId: 90
|
||||
|
||||
- Name: Snowpeak Ruins Dungeon Map
|
||||
Importance: Junk
|
||||
Id: 0xBA
|
||||
Dungeon Map: Snowpeak Ruins
|
||||
APItemId: 91
|
||||
|
||||
- Name: Temple of Time Dungeon Map
|
||||
Importance: Junk
|
||||
Id: 0xBB
|
||||
Dungeon Map: Temple of Time
|
||||
APItemId: 92
|
||||
|
||||
- Name: City in the Sky Dungeon Map
|
||||
Importance: Junk
|
||||
Id: 0xBC
|
||||
Dungeon Map: City in the Sky
|
||||
APItemId: 93
|
||||
|
||||
- Name: Palace of Twilight Dungeon Map
|
||||
Importance: Junk
|
||||
Id: 0xBD
|
||||
Dungeon Map: Palace of Twilight
|
||||
APItemId: 94
|
||||
|
||||
- Name: Hyrule Castle Dungeon Map
|
||||
Importance: Junk
|
||||
Id: 0xBE
|
||||
Dungeon Compass: Hyrule Castle
|
||||
APItemId: 95
|
||||
|
||||
- Name: Sacred Grove Portal
|
||||
Importance: Major
|
||||
Id: 0xBF
|
||||
APItemId: -1
|
||||
|
||||
- Name: Male Beetle
|
||||
Importance: Major
|
||||
Id: 0xC0
|
||||
APItemId: 96
|
||||
|
||||
- Name: Female Beetle
|
||||
Importance: Major
|
||||
Id: 0xC1
|
||||
APItemId: 97
|
||||
|
||||
- Name: Male Butterfly
|
||||
Importance: Major
|
||||
Id: 0xC2
|
||||
APItemId: 98
|
||||
|
||||
- Name: Female Butterfly
|
||||
Importance: Major
|
||||
Id: 0xC3
|
||||
APItemId: 99
|
||||
|
||||
- Name: Male Stag Beetle
|
||||
Importance: Major
|
||||
Id: 0xC4
|
||||
APItemId: 100
|
||||
|
||||
- Name: Female Stag Beetle
|
||||
Importance: Major
|
||||
Id: 0xC5
|
||||
APItemId: 101
|
||||
|
||||
- Name: Male Grasshopper
|
||||
Importance: Major
|
||||
Id: 0xC6
|
||||
APItemId: 102
|
||||
|
||||
- Name: Female Grasshopper
|
||||
Importance: Major
|
||||
Id: 0xC7
|
||||
APItemId: 103
|
||||
|
||||
- Name: Male Phasmid
|
||||
Importance: Major
|
||||
Id: 0xC8
|
||||
APItemId: 104
|
||||
|
||||
- Name: Female Phasmid
|
||||
Importance: Major
|
||||
Id: 0xC9
|
||||
APItemId: 105
|
||||
|
||||
- Name: Male Pill Bug
|
||||
Importance: Major
|
||||
Id: 0xCA
|
||||
APItemId: 106
|
||||
|
||||
- Name: Female Pill Bug
|
||||
Importance: Major
|
||||
Id: 0xCB
|
||||
APItemId: 107
|
||||
|
||||
- Name: Male Mantis
|
||||
Importance: Major
|
||||
Id: 0xCC
|
||||
APItemId: 108
|
||||
|
||||
- Name: Female Mantis
|
||||
Importance: Major
|
||||
Id: 0xCD
|
||||
APItemId: 109
|
||||
|
||||
- Name: Male Ladybug
|
||||
Importance: Major
|
||||
Id: 0xCE
|
||||
APItemId: 110
|
||||
|
||||
- Name: Female Ladybug
|
||||
Importance: Major
|
||||
Id: 0xCF
|
||||
APItemId: 111
|
||||
|
||||
- Name: Male Snail
|
||||
Importance: Major
|
||||
Id: 0xD0
|
||||
APItemId: 112
|
||||
|
||||
- Name: Female Snail
|
||||
Importance: Major
|
||||
Id: 0xD1
|
||||
APItemId: 113
|
||||
|
||||
- Name: Male Dragonfly
|
||||
Importance: Major
|
||||
Id: 0xD2
|
||||
APItemId: 114
|
||||
|
||||
- Name: Female Dragonfly
|
||||
Importance: Major
|
||||
Id: 0xD3
|
||||
APItemId: 115
|
||||
|
||||
- Name: Male Ant
|
||||
Importance: Major
|
||||
Id: 0xD4
|
||||
APItemId: 116
|
||||
|
||||
- Name: Female Ant
|
||||
Importance: Major
|
||||
Id: 0xD5
|
||||
APItemId: 117
|
||||
|
||||
- Name: Male Dayfly
|
||||
Importance: Major
|
||||
Id: 0xD6
|
||||
APItemId: 118
|
||||
|
||||
- Name: Female Dayfly
|
||||
Importance: Major
|
||||
Id: 0xD7
|
||||
APItemId: 119
|
||||
|
||||
- Name: Progressive Fused Shadow
|
||||
Importance: Major
|
||||
Id: 0xD8
|
||||
APItemId: 121
|
||||
|
||||
#- Name: Fused Shadow 2
|
||||
# Importance: Junk
|
||||
@@ -918,9 +1053,10 @@
|
||||
# Importance: Junk
|
||||
# Id: 0xDB
|
||||
|
||||
#- Name: Unused
|
||||
# Importance: Junk
|
||||
# Id: 0xDC
|
||||
- Name: Archipelago Item
|
||||
Importance: Minor
|
||||
Id: 0xDC
|
||||
APItemId: -1
|
||||
|
||||
#- Name: Unused
|
||||
# Importance: Junk
|
||||
@@ -937,10 +1073,12 @@
|
||||
- Name: Poe Soul
|
||||
Importance: Major
|
||||
Id: 0xE0
|
||||
APItemId: 124
|
||||
|
||||
- Name: Progressive Hidden Skill # Also Ending Blow
|
||||
Importance: Major
|
||||
Id: 0xE1
|
||||
APItemId: 122
|
||||
|
||||
#- Name: Shield Attack
|
||||
# Importance: Major
|
||||
@@ -969,10 +1107,12 @@
|
||||
- Name: Bridge of Eldin Portal
|
||||
Importance: Major
|
||||
Id: 0xE8
|
||||
APItemId: -1
|
||||
|
||||
- Name: Progressive Sky Book
|
||||
Importance: Major
|
||||
Id: 0xE9
|
||||
APItemId: 123
|
||||
|
||||
#- Name: Partially Filled Sky Book
|
||||
# Importance: Major
|
||||
@@ -989,10 +1129,12 @@
|
||||
- Name: Purple Rupee Links House
|
||||
Importance: Junk
|
||||
Id: 0xED
|
||||
APItemId: 7
|
||||
|
||||
- Name: North Faron Woods Gate Key
|
||||
Importance: Major
|
||||
Id: 0xEE
|
||||
APItemId: 63
|
||||
|
||||
#- Name: Blue Fire
|
||||
# Importance: Major
|
||||
@@ -1013,19 +1155,23 @@
|
||||
- Name: Gate Keys
|
||||
Importance: Major
|
||||
Id: 0xF3
|
||||
APItemId: 65
|
||||
|
||||
- Name: Ordon Pumpkin
|
||||
Importance: Major
|
||||
Id: 0xF4
|
||||
APItemId: 129
|
||||
|
||||
- Name: Ordon Cheese
|
||||
Importance: Major
|
||||
Id: 0xF5
|
||||
APItemId: 130
|
||||
|
||||
- Name: Snowpeak Ruins Bedroom Key
|
||||
Importance: Major
|
||||
Id: 0xF6
|
||||
Dungeon Big Key: Snowpeak Ruins
|
||||
APItemId: 73
|
||||
|
||||
#- Name: Surfboard (Unused)
|
||||
# Importance: Major
|
||||
@@ -1039,6 +1185,7 @@
|
||||
Importance: Major
|
||||
Id: 0xF9
|
||||
Dungeon Big Key: Goron Mines
|
||||
APItemId: 71
|
||||
|
||||
#- Name: Got Lantern Back
|
||||
# Importance: Major
|
||||
@@ -1059,6 +1206,7 @@
|
||||
- Name: Coro Key
|
||||
Importance: Major
|
||||
Id: 0xFE
|
||||
APItemId: 64
|
||||
|
||||
#- Name: Invalid
|
||||
# Importance: Major
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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: |-
|
||||
<fast>You got an <red>Archipelago Item<white>!
|
||||
|
||||
Foolish Get Item Text:
|
||||
Standard:
|
||||
Text: |-
|
||||
|
||||
@@ -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: |-
|
||||
<fast>You got an <red>Archipelago Item<white>!
|
||||
|
||||
Foolish Get Item Text:
|
||||
Standard:
|
||||
Text: |-
|
||||
|
||||
@@ -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: |-
|
||||
<fast>You got an <red>Archipelago Item<white>!
|
||||
|
||||
Foolish Get Item Text:
|
||||
Standard:
|
||||
Text: |-
|
||||
|
||||
@@ -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: |-
|
||||
<fast>You got an <red>Archipelago Item<white>!
|
||||
|
||||
Foolish Get Item Text:
|
||||
Standard:
|
||||
Text: |-
|
||||
|
||||
@@ -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: |-
|
||||
<fast>You got an <red>Archipelago Item<white>!
|
||||
|
||||
Foolish Get Item Text:
|
||||
Standard:
|
||||
Text: |-
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -4,36 +4,33 @@
|
||||
#include "world.hpp"
|
||||
|
||||
namespace randomizer::logic::hints {
|
||||
static const std::unordered_map<std::string, std::string> dungeonColors = {
|
||||
{"Forest Temple", "<green>"},
|
||||
{"Goron Mines", "<red>"},
|
||||
{"Lakebed Temple", "<blue>"},
|
||||
{"Arbiters Grounds", "<orange>"},
|
||||
{"Snowpeak Ruins", "<light blue>"},
|
||||
{"Temple of Time", "<dark green>"},
|
||||
{"City in the Sky", "<yellow>"},
|
||||
{"Palace of Twilight", "<purple>"},
|
||||
// {"Hyrule Castle", "<silver>"}
|
||||
};
|
||||
|
||||
// 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<std::string, std::string> dungeonColors = {
|
||||
{"Forest Temple", "<green>"},
|
||||
{"Goron Mines", "<red>"},
|
||||
{"Lakebed Temple", "<blue>"},
|
||||
{"Arbiters Grounds", "<orange>"},
|
||||
{"Snowpeak Ruins", "<light blue>"},
|
||||
{"Temple of Time", "<dark green>"},
|
||||
{"City in the Sky", "<yellow>"},
|
||||
{"Palace of Twilight", "<purple>"},
|
||||
// {"Hyrule Castle", "<silver>"}
|
||||
};
|
||||
|
||||
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::World>& world,
|
||||
static void doItemTextReplacement(world::World* world,
|
||||
const std::string& locationName,
|
||||
const std::list<std::string>& textNames,
|
||||
Text::Color color) {
|
||||
@@ -50,40 +47,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::World>& world) {
|
||||
GenerateRequiredDungeonsHint(world.get());
|
||||
GenerateItemTextReplacements(world.get());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,5 +3,6 @@
|
||||
namespace randomizer::logic::hints {
|
||||
|
||||
void GenerateAllHints(world::WorldPool& worldPool);
|
||||
void GenerateAllHints(const std::unique_ptr<world::World>& world);
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <iostream>
|
||||
|
||||
#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<logic::world::World> world = std::make_unique<logic::world::World>(1, this);
|
||||
world->SetSettings(this->_config.GetSettingsList().front());
|
||||
// Always use logic when building a tracker world
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace randomizer
|
||||
*/
|
||||
std::optional<std::string> Generate();
|
||||
void GenerateWorlds();
|
||||
void GenerateTrackerWorld();
|
||||
void GenerateTrackerWorld(bool useAntiSpoilerLog = true);
|
||||
|
||||
auto& GetConfig() { return this->_config; }
|
||||
auto& GetWorlds() { return this->_worlds; }
|
||||
|
||||
@@ -53,12 +53,19 @@ FetchContent_Declare(
|
||||
GIT_TAG fdbae3f
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(yaml-cpp base64pp battery-embed)
|
||||
message(STATUS "randomizer: Fetching APCpp")
|
||||
FetchContent_Declare(
|
||||
APCpp
|
||||
GIT_REPOSITORY https://github.com/CraftyBoss/APCpp.git
|
||||
GIT_TAG 2d92f75
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(yaml-cpp base64pp battery-embed APCpp)
|
||||
|
||||
string(LENGTH "${CMAKE_SOURCE_DIR}/" SOURCE_PATH_SIZE)
|
||||
set(GAME_COMPILE_DEFS ${GAME_COMPILE_DEFS} SOURCE_PATH_SIZE=${SOURCE_PATH_SIZE})
|
||||
set(GAME_LIBS ${GAME_LIBS} yaml-cpp::yaml-cpp base64pp)
|
||||
set(GAME_LIBS ${GAME_LIBS} yaml-cpp::yaml-cpp base64pp APCpp)
|
||||
|
||||
file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/randomizer")
|
||||
# Put data files together for easier manipulation
|
||||
# file(COPY "${CMAKE_SOURCE_DIR}/src/dusk/randomizer/data/" DESTINATION "${CMAKE_BINARY_DIR}/randomizer/data/" REGEX "^.*example.*$" EXCLUDE) # World, macros, and location info
|
||||
# file(COPY "${CMAKE_SOURCE_DIR}/src/dusk/randomizer/data/" DESTINATION "${CMAKE_BINARY_DIR}/randomizer/data/" REGEX "^.*example.*$" EXCLUDE) # World, macros, and location info
|
||||
@@ -212,6 +212,24 @@ UserSettings g_userSettings = {
|
||||
ConfigVar<std::string>{"randomizer.file2SeedHash", ""},
|
||||
ConfigVar<std::string>{"randomizer.file3SeedHash", ""},
|
||||
},
|
||||
|
||||
.archipelago = {
|
||||
{
|
||||
ConfigVar<std::string>{"archipelago.serverIP_file1", "archipelago.gg"},
|
||||
ConfigVar<std::string>{"archipelago.serverIP_file2", "archipelago.gg"},
|
||||
ConfigVar<std::string>{"archipelago.serverIP_file3", "archipelago.gg"}
|
||||
},
|
||||
{
|
||||
ConfigVar<std::string>{"archipelago.serverPass_file1", ""},
|
||||
ConfigVar<std::string>{"archipelago.serverPass_file2", ""},
|
||||
ConfigVar<std::string>{"archipelago.serverPass_file3", ""}
|
||||
},
|
||||
{
|
||||
ConfigVar<std::string>{"archipelago.slotName_file1", ""},
|
||||
ConfigVar<std::string>{"archipelago.slotName_file2", ""},
|
||||
ConfigVar<std::string>{"archipelago.slotName_file3", ""}
|
||||
}
|
||||
},
|
||||
|
||||
.cosmetics = {
|
||||
.herosTunicCapColor = {"cosmetics.hatColor", ""},
|
||||
@@ -407,6 +425,18 @@ void registerSettings() {
|
||||
Register(g_userSettings.randomizer.seedHashes[0]);
|
||||
Register(g_userSettings.randomizer.seedHashes[1]);
|
||||
Register(g_userSettings.randomizer.seedHashes[2]);
|
||||
|
||||
Register(g_userSettings.archipelago.savesServerIP[0]);
|
||||
Register(g_userSettings.archipelago.savesServerIP[1]);
|
||||
Register(g_userSettings.archipelago.savesServerIP[2]);
|
||||
|
||||
Register(g_userSettings.archipelago.savesServerPass[0]);
|
||||
Register(g_userSettings.archipelago.savesServerPass[1]);
|
||||
Register(g_userSettings.archipelago.savesServerPass[2]);
|
||||
|
||||
Register(g_userSettings.archipelago.savesSlotName[0]);
|
||||
Register(g_userSettings.archipelago.savesSlotName[1]);
|
||||
Register(g_userSettings.archipelago.savesSlotName[2]);
|
||||
|
||||
Register(g_userSettings.cosmetics.herosTunicCapColor);
|
||||
Register(g_userSettings.cosmetics.herosTunicTorsoColor);
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
#include <dusk/ui/archi_connect_modal.hpp>
|
||||
#include <dusk/ui/string_button.hpp>
|
||||
|
||||
#include <thread>
|
||||
#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(dComIfGs_getDataNum(), 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);
|
||||
|
||||
int dataNum = dComIfGs_getDataNum();
|
||||
|
||||
archi::ArchipelagoContext::SetServerIp(textModal->get_input_text(0), dataNum);
|
||||
archi::ArchipelagoContext::SetPassword(textModal->get_input_text(1), dataNum);
|
||||
archi::ArchipelagoContext::SetSlotName(textModal->get_input_text(2), dataNum);
|
||||
|
||||
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);
|
||||
|
||||
int dataNum = dComIfGs_getDataNum();
|
||||
|
||||
inputModal->add_input_text("Server IP", archi::ArchipelagoContext::GetServerIp(dataNum));
|
||||
inputModal->add_input_text("Password", archi::ArchipelagoContext::GetPassword(dataNum));
|
||||
inputModal->add_input_text("Slot Name", archi::ArchipelagoContext::GetSlotName(dataNum));
|
||||
}
|
||||
|
||||
void BeginArchipelagoConnectionUI(bool forceChangeConnection) {
|
||||
if (forceChangeConnection) {
|
||||
CreateSetupConnectionInfoModal();
|
||||
return;
|
||||
}
|
||||
|
||||
int dataNum = dComIfGs_getDataNum();
|
||||
bool hasConnectInfo = (!archi::ArchipelagoContext::GetServerIp(dataNum).empty() && !archi::ArchipelagoContext::GetSlotName(dataNum).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