Compare commits

...

22 Commits

Author SHA1 Message Date
CraftyBoss 589c6675fe fix inventory reset ignoring previously obtained items on inv fill, add heros clothes to reset inventory default 2026-07-02 00:50:07 -07:00
CraftyBoss 0be48db605 save archipelago connection info based on current save slot 2026-07-01 01:36:14 -07:00
CraftyBoss dbb9ce2c03 fix merge error 2026-07-01 00:51:29 -07:00
CraftyBoss cc808ab50c Merge remote-tracking branch 'origin/randomizer' into rando-archi
# Conflicts:
#	include/dusk/settings.h
#	src/dusk/settings.cpp
2026-07-01 00:42:52 -07:00
CraftyBoss 7682165478 Merge remote-tracking branch 'origin/randomizer' into rando-archi 2026-06-30 01:08:17 -07:00
CraftyBoss 57019bea30 Merge remote-tracking branch 'origin/main' into rando-archi 2026-06-25 01:05:26 -07:00
CraftyBoss c590018059 trigger game complete on ganondorf kill
with this, in theory AP seeds should be beatable now, of course theres still a ton of work to do in terms of fleshing out some features
2026-06-25 01:05:15 -07:00
CraftyBoss e7eefbb0cb Merge remote-tracking branch 'origin/randomizer' into rando-archi 2026-06-24 03:22:56 -07:00
CraftyBoss 7ee39edee7 add death link support, add needed archipelago item messages to language yamls
Archipelago items will still be green rupees for now until we decide how we want to handle custom game assets
2026-06-24 03:22:42 -07:00
CraftyBoss 5a1368b355 cleanup settings parser, get settings directly from slot data instead of user YAML
loaded seeds should now have proper settings applied without needing to seperately parse the archi's player settings yaml.
2026-06-22 23:43:24 -07:00
CraftyBoss 6984ae83bf add UI to selecting/creating archipelago seed
Next step will be to associate connection info to each save slot instead of overwriting the same config values for every slot. With these changes though, it should be possible to connect to an archipelago lobby without needing to use the ImGui debug window.
2026-06-22 03:25:01 -07:00
CraftyBoss 93479100fc Merge remote-tracking branch 'origin/randomizer' into rando-archi 2026-06-21 14:36:49 -07:00
CraftyBoss 6366ab61d1 add inventory resetting on server connect, properly handle getting new items from locations, integrate some of archi into new rando ui stuff
For the most part, solo archipelago runs seem to work fine now using the original TP apworld, but im sure there are plenty of weird issues still that need to be handled.
2026-06-19 17:07:42 -07:00
CraftyBoss 7537f1c4a8 Merge remote-tracking branch 'origin/randomizer' into rando-archi 2026-06-19 17:01:52 -07:00
CraftyBoss 59b76f7aae Merge remote-tracking branch 'origin/randomizer' into rando-archi 2026-06-12 23:29:45 -07:00
CraftyBoss 4466442cfb add location IDs to yaml db, get started on translating received ap data to rando data
the idea is to use the existing randomizer systems (like World, RandomizerContext, etc) in order to generate necessary data for in-game modifications such as text replacements, model swaps, etc. to do this we need to get the apworld's current settings, which atm will require loading the configured ap yaml file before connecting to archipelago.
2026-06-12 02:47:52 -07:00
CraftyBoss d6f741947a Merge remote-tracking branch 'origin/randomizer' into rando-archi 2026-06-10 22:59:44 -07:00
CraftyBoss 51a25206c2 Merge remote-tracking branch 'origin/randomizer' into rando-archi
# Conflicts:
#	include/dusk/settings.h
#	src/dusk/settings.cpp
2026-06-10 21:02:31 -07:00
CraftyBoss f6b86d3f38 actually get some ap functionality going
getting items from the server works now mostly, for now we're ignoring inventory fills as its going to require a bit of work to reset the inventory on connection.
2026-06-09 01:23:05 -07:00
CraftyBoss b118fa19e1 Merge remote-tracking branch 'origin/randomizer' into rando-archi 2026-06-09 01:14:16 -07:00
CraftyBoss efbf82d4c7 Merge remote-tracking branch 'origin/randomizer' into rando-archi 2026-06-08 16:44:48 -07:00
CraftyBoss 83adafff82 basic setup for archipelago support started
For now we'll be using APCpp, seems to be good enough for what we need to do for this. might eventually roll my own library if things end up being too weird/complicated to integrate properly.
2026-06-06 19:39:23 -07:00
46 changed files with 2818 additions and 113 deletions
+5
View File
@@ -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
+32
View File
@@ -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);
}
+4
View File
@@ -205,6 +205,7 @@ public:
DATASELPROC_SELECT_DATA_NAME_MOVE,
#if TARGET_PC
DATASELPROC_SELECT_DATA_PLAY_MOVE, // Select between vanilla or randomizer play
DATASELPROC_MENU_ARCHIPELAGO_CONNECT, // Wait for archipelago to connect
#endif
DATASELPROC_SELECT_DATA_OPENERASE_MOVE,
DATASELPROC_MENU_SELECT,
@@ -336,6 +337,7 @@ public:
void selectDataNameMove();
#if TARGET_PC
void selectDataPlayTypeMove();
void menuArchipelagoConnect();
#endif
void selectDataOpenEraseMove();
void menuSelect();
@@ -742,6 +744,8 @@ public:
dDlst_FileSelFade_c mFadeDlst;
bool mStartNameAnm;
bool mBackToFileSelect;
bool mArchipelagoBeginConnect;
bool mArchiStartCloseFile;
int mPendingRmlCloseFrames{0};
} mDusk;
#endif
+1
View File
@@ -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
+1 -1
View File
@@ -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,
+3
View File
@@ -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);
+7
View File
@@ -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 {
+15
View File
@@ -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;
+40
View File
@@ -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);
}
+5 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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() {
+26
View File
@@ -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
+60
View File
@@ -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();
}
}
+19
View File
@@ -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();
};
}
+2
View File
@@ -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() {
+2
View File
@@ -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;
+12 -2
View File
@@ -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
+76
View File
@@ -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>());
+5
View File
@@ -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.
*/
+151 -3
View File
@@ -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;
+50 -49
View File
@@ -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 -9
View File
@@ -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
+1 -1
View File
@@ -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; }
+10 -3
View File
@@ -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
+30
View File
@@ -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);
+168
View File
@@ -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();
}
}
}
+29
View File
@@ -0,0 +1,29 @@
#pragma once
#include "modal.hpp"
namespace dusk::ui
{
class ArchiConnectModal : public Modal {
public:
enum class ConnectionStatus {
None,
Ready,
Disconnecting,
Connecting,
Generating,
Success,
Error,
};
explicit ArchiConnectModal();
void update() override;
private:
ConnectionStatus mDisplayedStatus = ConnectionStatus::None;
};
void BeginArchipelagoConnectionUI(bool forceChangeConnection = false);
}
+63
View File
@@ -133,4 +133,67 @@ void TextInputModal::update() {
Document::update();
}
MultiTextInputModal::MultiTextInputModal(Props props) : Modal(props) {}
void MultiTextInputModal::add_input_text(const Rml::String& label, const Rml::String& startValue) {
auto modalBody = mDialog->QuerySelector(".modal-body");
bool isFirst = mInputs.empty();
mInputs.push_back(InputEntry {});
auto& entry = mInputs.back();
int idx = mInputs.size() - 1;
entry.text = startValue;
entry.button = std::make_unique<StringButton>(modalBody, StringButton::Props{
.key = label,
.getValue = [idx, this]{return mInputs[idx].text;},
.setValue = [idx, this](Rml::String value) { mInputs[idx].text = value; },
});
if (isFirst)
entry.button->start_editing();
}
bool MultiTextInputModal::handle_nav_command(Rml::Event& event, NavCommand cmd) {
auto retVal = Modal::handle_nav_command(event, cmd);
if (!retVal) {
if (cmd == NavCommand::Down) {
if (mTextSelIdx < mInputs.size()) {
mTextSelIdx++;
if (mTextSelIdx >= mInputs.size()) {
mButtons[0]->focus();
}else {
mInputs[mTextSelIdx].button->focus();
}
}else {
mTextSelIdx = 0;
mInputs[mTextSelIdx].button->focus();
}
mDoAud_seStartMenu(kSoundItemFocus);
retVal = true;
}else if (cmd == NavCommand::Up && mTextSelIdx >= 0) {
mTextSelIdx--;
if (mTextSelIdx < 0) {
mTextSelIdx = mInputs.size();
mButtons[0]->focus();
}else {
mInputs[mTextSelIdx].button->focus();
}
mDoAud_seStartMenu(kSoundItemFocus);
retVal = true;
}
}
return retVal;
}
void MultiTextInputModal::update() {
for (const auto& input : mInputs) {
input.button->update();
}
Modal::update();
}
} // namespace dusk::ui
+23
View File
@@ -53,4 +53,27 @@ private:
Rml::String mInputText{};
};
class MultiTextInputModal : public Modal {
public:
explicit MultiTextInputModal(Props props);
void add_input_text(const Rml::String& label, const Rml::String& startValue = "");
Rml::String get_input_text(int idx) {return mInputs[idx].text; }
void update() override;
protected:
bool handle_nav_command(Rml::Event& event, NavCommand cmd) override;
private:
struct InputEntry {
std::unique_ptr<StringButton> button;
Rml::String text;
};
std::vector<InputEntry> mInputs{};
int mTextSelIdx = 0;
};
} // namespace dusk::ui