From 80f4284e8277df3fb2a2872d268c936635d44734 Mon Sep 17 00:00:00 2001 From: gymnast86 Date: Sat, 25 Apr 2026 03:07:07 -0700 Subject: [PATCH] implement item queue --- include/d/actor/d_a_alink.h | 4 + include/d/d_com_inf_game.h | 9 + include/d/d_meter2_draw.h | 4 +- include/d/d_msg_flow.h | 3 + include/d/d_stage.h | 3 + src/d/actor/d_a_alink_demo.inc | 17 + src/d/actor/d_a_b_ds.cpp | 7 + src/d/d_msg_flow.cpp | 6 + .../randomizer/game/randomizer_context.cpp | 319 ++++++++++++++++++ .../randomizer/game/randomizer_context.hpp | 61 +++- src/f_pc/f_pc_manager.cpp | 21 ++ 11 files changed, 452 insertions(+), 2 deletions(-) diff --git a/include/d/actor/d_a_alink.h b/include/d/actor/d_a_alink.h index 9980ab776f..692a424961 100644 --- a/include/d/actor/d_a_alink.h +++ b/include/d/actor/d_a_alink.h @@ -3908,6 +3908,10 @@ public: u16 getReadyItem() { return dComIfGp_getSelectItem(mSelectItemId); } static u32 getOtherHeapSize() { return 0xF0A60; } + +#if TARGET_PC + u16 getEventId() { return mMsgFlow.getEventId(); } +#endif static daAlink_BckData const m_mainBckShield[20]; static daAlink_BckData const m_mainBckSword[5]; diff --git a/include/d/d_com_inf_game.h b/include/d/d_com_inf_game.h index 91714031d2..3bf686f37c 100644 --- a/include/d/d_com_inf_game.h +++ b/include/d/d_com_inf_game.h @@ -470,6 +470,9 @@ public: s8 getNextStageRoomNo() { return mNextStage.getRoomNo(); } s8 getNextStageLayer() { return mNextStage.getLayer(); } BOOL isEnableNextStage() { return mNextStage.isEnable(); } +#if TARGET_PC + void setEnableNextStage() { return mNextStage.setEnable(); } +#endif void offEnableNextStage() { mNextStage.offEnable(); } s8 getNextStageWipe() { return mNextStage.getWipe(); } u8 getNextStageWipeSpeed() { return mNextStage.getWipeSpeed(); } @@ -2542,6 +2545,12 @@ inline void dComIfGp_offEnableNextStage() { g_dComIfG_gameInfo.play.offEnableNextStage(); } +#if TARGET_PC +inline void dComIfGp_setEnableNextStage() { + g_dComIfG_gameInfo.play.setEnableNextStage(); +} +#endif + inline s8 dComIfGp_getNextStageWipe() { return g_dComIfG_gameInfo.play.getNextStageWipe(); } diff --git a/include/d/d_meter2_draw.h b/include/d/d_meter2_draw.h index 10cded803d..0573d538b3 100644 --- a/include/d/d_meter2_draw.h +++ b/include/d/d_meter2_draw.h @@ -129,7 +129,9 @@ public: bool getPlayerSubject(); bool isBButtonShow(bool); s16 getButtonTimer(); - +#if TARGET_PC + f32 getZButtonAlpha() { return mButtonZAlpha;} +#endif virtual ~dMeter2Draw_c(); J2DScreen* getMainScreenPtr() { return mpScreen; } diff --git a/include/d/d_msg_flow.h b/include/d/d_msg_flow.h index cd9a7c4e5f..7a6a26f3f9 100644 --- a/include/d/d_msg_flow.h +++ b/include/d/d_msg_flow.h @@ -58,6 +58,9 @@ public: int checkEventRender(int*, int*, int*, int*); void remove(); u16 getEventId(int*); +#if TARGET_PC + u16 getEventId(); +#endif u32 getMsgNo(); u32 getNowMsgNo(); msg_class* getMsg(); diff --git a/include/d/d_stage.h b/include/d/d_stage.h index bd8e7d5728..3b2a94a019 100644 --- a/include/d/d_stage.h +++ b/include/d/d_stage.h @@ -1292,6 +1292,9 @@ public: void set(const char*, s8, s16, s8, s8, u8); void offEnable() { enabled = 0; } BOOL isEnable() const { return enabled; } +#if TARGET_PC + void setEnable() { enabled |= 0x1; } +#endif s8 getWipe() const { return wipe; } u8 getWipeSpeed() const { return wipe_speed; } dStage_startStage_c* getStartStage() { return this; } diff --git a/src/d/actor/d_a_alink_demo.inc b/src/d/actor/d_a_alink_demo.inc index 49f4c5c4d8..1fe5eb3432 100644 --- a/src/d/actor/d_a_alink_demo.inc +++ b/src/d/actor/d_a_alink_demo.inc @@ -23,7 +23,10 @@ #include "d/actor/d_a_npc_tkc.h" #include +#if TARGET_PC #include "dusk/settings.h" +#include "dusk/randomizer/game/randomizer_context.hpp" +#endif BOOL daAlink_c::checkEventRun() const { return dComIfGp_event_runCheck() || checkPlayerDemoMode(); @@ -2535,6 +2538,15 @@ int daAlink_c::procCoGetItemInit() { s16 var_r22 = 0; BOOL var_r31 = FALSE; BOOL var_r30 = FALSE; +#if TARGET_PC + if (randomizer_IsActive()) { + // If we are giving a custom item, we want to set mParam0 to 0x100 so that instead of trying to search for an item + // actor that doesnt exist we want the game to create one using the item id in mGtItm. + if (g_randomizerState.getGiveItemToPlayerStatus() == RandomizerState::ITEM_IN_QUEUE) { + mDemo.setParam0(0x100); + } + } +#endif if (mProcID == PROC_GET_ITEM || mProcID == PROC_INSECT_CATCH || (mProcID == PROC_PREACTION_UNEQUIP && !checkNoUpperAnime())) { @@ -2572,6 +2584,11 @@ int daAlink_c::procCoGetItemInit() { } else { item_no = dComIfGp_event_getGtItm(); } +#if TARGET_PC + if (randomizer_IsActive() && g_randomizerState.getGiveItemToPlayerStatus() == RandomizerState::ITEM_IN_QUEUE) { + g_randomizerState.setGiveItemToPlayerStatus(RandomizerState::CLEAR_QUEUE); + } +#endif fpc_ProcID item_partner_id = fopAcM_createItemForPresentDemo(¤t.pos, item_no, 0, -1, fopAcM_GetRoomNo(this), NULL, NULL); diff --git a/src/d/actor/d_a_b_ds.cpp b/src/d/actor/d_a_b_ds.cpp index 487addd826..3ecba6f4a1 100644 --- a/src/d/actor/d_a_b_ds.cpp +++ b/src/d/actor/d_a_b_ds.cpp @@ -4084,6 +4084,13 @@ void daB_DS_c::executeBattle2Dead() { camera->mCamera.SetTrimSize(0); dComIfGp_event_reset(); dComIfGs_onStageBossEnemy(0x13); +#if TARGET_PC + if (randomizer_IsActive()) { + // Give the boss item + u8 agDungeonReward = randomizer_getItemAtLocation("Arbiters Grounds Dungeon Reward"); + g_randomizerState.addItemToEventQueue(agDungeonReward); + } +#endif /* dSv_event_flag_c::F_0265 - Arbiter's Grounds - Arbiter's Grounds clear */ dComIfGs_onEventBit(0x2010); fopAcM_delete(this); diff --git a/src/d/d_msg_flow.cpp b/src/d/d_msg_flow.cpp index 51d80dcfbf..aa9256f44c 100644 --- a/src/d/d_msg_flow.cpp +++ b/src/d/d_msg_flow.cpp @@ -260,6 +260,12 @@ u16 dMsgFlow_c::getEventId(int* oItemId_p) { return mEventId; } +#if TARGET_PC +u16 dMsgFlow_c::getEventId() { + return mEventId; +} +#endif + u32 dMsgFlow_c::getMsgNo() { return mMsgNo; } diff --git a/src/dusk/randomizer/game/randomizer_context.cpp b/src/dusk/randomizer/game/randomizer_context.cpp index 0fe39ef986..b219cc3e94 100644 --- a/src/dusk/randomizer/game/randomizer_context.cpp +++ b/src/dusk/randomizer/game/randomizer_context.cpp @@ -4,6 +4,8 @@ #include "dusk/logging.h" #include "dusk/main.h" #include "dusk/randomizer/game/tools.h" +#include "dusk/randomizer/game/stages.h" +#include "dusk/randomizer/game/verify_item_functions.h" #include "dusk/randomizer/generator/utility/endian.hpp" #include "dusk/randomizer/generator/utility/yaml.hpp" #include "dusk/randomizer/generator/randomizer.hpp" @@ -14,6 +16,10 @@ #include #include "d/actor/d_a_alink.h" +#include "d/d_com_inf_game.h" +#include "d/d_meter2_info.h" +#include "d/d_meter2.h" +#include "d/d_meter2_draw.h" std::optional RandomizerContext::WriteToFile() { @@ -46,6 +52,9 @@ std::optional RandomizerContext::WriteToFile() { } } + const std::unordered_map u16PoeOverrides(this->mPoeOverrides.begin(), this->mPoeOverrides.end()); + out["mPoeOverrides"] = u16PoeOverrides; + for (const auto& [stageIdx, itemOverride] : this->mFreestandingItemOverrides) { for (const auto& [flag, itemId] : itemOverride) { out["mFreestandingItemOverrides"][static_cast(stageIdx)][static_cast(flag)] = static_cast(itemId); @@ -119,6 +128,13 @@ std::optional RandomizerContext::LoadFromHash(const std::string& ha } } + // Poe Overrides + for (const auto& poeNode : in["mPoeOverrides"]) { + u16 key = poeNode.first.as(); + u8 itemId = poeNode.second.as(); + this->mPoeOverrides[key] = itemId; + } + // Freestanding overrides for (const auto& stageNode : in["mFreestandingItemOverrides"]) { const auto& stageIdx = stageNode.first.as(); @@ -180,6 +196,296 @@ std::string RandomizerContext::GetSeedDataPath() const { return std::string(SDL_GetPrefPath(dusk::OrgName, dusk::AppName)) + "randomizer/seeds/" + this->mHash + "/seed.dat"; } +RandomizerState g_randomizerState; + +int RandomizerState::_create() { + mInitialized = true; + mEventItemStatus = QUEUE_EMPTY; + mHasPendingToDChange = false; + // g_customMenuRing._initialize(); + return 1; +} + +int RandomizerState::_delete() { + mInitialized = false; + return 1; +} + +int RandomizerState::execute() { + if (!mInitialized) { + return 0; + } + + // Always check for and handle time of day changes + if (getTimeChange() != NO_CHANGE) { + handleTimeSpeed(); + } + + bool currentReloadingState; + // Any custom functionality that relies on Link's actor being on a stage + if (daAlink_getAlinkActorClass()) { + currentReloadingState = daAlink_getAlinkActorClass()->checkRestartRoom(); + // Handle giving item to the player at any time. + initGiveItemToPlayer(); + } + else { + currentReloadingState = true; + } + + bool prevReloadingState = getRoomReloadingState(); + if (!currentReloadingState) { + if (prevReloadingState) { + offLoad(); + } + } + setRoomReloadingState(currentReloadingState); + + // COpypasta old rando code until I build the framework out. + /*if (!libtp::tp::d_a_alink::checkStageName(libtp::data::stage::allStages[libtp::data::stage::StageIDs::Title_Screen])) + { + handleFoolishItem(randoPtr); + }*/ + + return 1; +} + +int RandomizerState::draw() { + return 1; +} + +void RandomizerState::handlePoeItem(u8 bitSw) +{ + u16 key = getStageID() << 8 | bitSw; + u8 item = randomizer_GetContext().mPoeOverrides[key]; + addItemToEventQueue(item); + daAlink_getAlinkActorClass()->procWolfAtnActorMoveInit(); +} + +void RandomizerState::addItemToEventQueue(u8 item) +{ + for (int i = 0; i < EVENT_ITEM_QUEUE_SIZE; i++) + { + if (mEventItemQueue[i] == 0) + { + mEventItemQueue[i] = item; + break; + } + } +} + +void RandomizerState::initGiveItemToPlayer() +{ + switch (daAlink_getAlinkActorClass()->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 (daAlink_getAlinkActorClass()->checkEventRun()) + { + break; + } + + // Ensure that link is not currently in a message-based event. + if (daAlink_getAlinkActorClass()->getEventId() != 0) + { + break; + } + + u8 itemToGive = 0xFF; + + for (int i = 0; i < EVENT_ITEM_QUEUE_SIZE; i++) + { + const u8 storedItem = mEventItemQueue[i]; + + if (storedItem) + { + const u8 giveItemToPlayerStatus = getGiveItemToPlayerStatus(); + + // If we have the call to clear the queue, then we want to clear the item and break out. + if (giveItemToPlayerStatus == CLEAR_QUEUE) + { + mEventItemQueue[i] = 0; + setGiveItemToPlayerStatus(QUEUE_EMPTY); + break; + } + + // If the queue is empty and we have an item to give, update the queue state. + else if (giveItemToPlayerStatus == QUEUE_EMPTY) + { + setGiveItemToPlayerStatus(ITEM_IN_QUEUE); + } + + itemToGive = verifyProgressiveItem(storedItem); + break; + } + } + + // if there is no item to give, break out of the case. + if (itemToGive == 0xFF) + { + break; + } + + g_dComIfG_gameInfo.play.getEvent()->setGtItm(itemToGive); + + // Set the process value for getting an item to start the "get item" cutscene when next available. + daAlink_getAlinkActorClass()->mProcID = daAlink_c::PROC_GET_ITEM; + + // Get the event index for the "Get Item" event. + const s16 eventIdx = dComIfGp_getEventManager().getEventIdx((fopAc_ac_c*)daAlink_getAlinkActorClass(),"DEFAULT_GETITEM",0xFF); + + // Finally we want to modify the event stack to prioritize our custom event so that it happens next. + fopAcM_orderChangeEventId(daAlink_getAlinkActorClass(), eventIdx, 1, 0xFFFF); + } + default: + { + break; + } + } +} + +void RandomizerState::handleTimeOfDayChange() +{ + if (dComIfGp_roomControl_getTimePass()) + { + // No point in changing values if we are already changing the time. + if (getTimeChange() == NO_CHANGE) + { + if (!dKy_daynight_check()) // Day time + { + setTimeChange(CHANGE_TO_NIGHT); + } + else + { + setTimeChange(CHANGE_TO_DAY); + } + g_env_light.time_change_rate = 1.f; // Increase time speed + } + } + else + { + if (!dKy_daynight_check()) // Day time + { + dComIfGs_setTime(285.f); + } + else + { + dComIfGs_setTime(105.f); + } + dComIfGp_setEnableNextStage(); + } +} + +void RandomizerState::handleTimeSpeed() +{ + + if (!dKy_daynight_check()) // Day time + { + if (getTimeChange() == CHANGE_TO_DAY) + { + g_env_light.time_change_rate = 0.012f; // Set time speed to normal + setTimeChange(NO_CHANGE); + } + } + else if (getTimeChange() == CHANGE_TO_NIGHT) + { + g_env_light.time_change_rate = 0.012f; // Set time speed to normal + setTimeChange(NO_CHANGE); + } +} + +void RandomizerState::offLoad() +{ + if ((getStageID() == City_in_the_Sky) && (dStage_roomControl_c::mStayNo == 0) && (dComIfGp_getStartStagePoint() == 3)) + { + // Fan in the main room active + dComIfGs_offSaveSwitch(0xA); + + // Main Room 1F explored + dComIfGs_offSaveSwitch(0xF); + } + + if (playerIsInRoomStage(1, allStages[Sacred_Grove])) + { + // If the portal in SG isn't active then we want to spawn the shadow beasts. + if (!dComIfGs_isSaveSwitch(0x64)) + { + dComIfGs_onSvOneZoneSwitch(0, 0xE); + } + } + + if ((getStageID() == Ordon_Ranch) && (dComIfGp_getStartStagePoint() == 1)) + { + // Clear the danBit that starts a conversation when entering the ranch so the player can do goats as needed. + dComIfGs_offSaveDunSwitch(0x1); + } +} + +bool checkFoolishItemEffectReady() +{ + // Verify Link is loaded on the map. + if (!daAlink_getAlinkActorClass()) + { + return false; + } + + // Ensure Link is not in a cutscene + if (daAlink_getAlinkActorClass()->checkEventRun()) + { + return false; + } + + // Make sure Link isn't riding anything + if (daAlink_getAlinkActorClass()->checkRide()) + { + return false; + } + + // Ensure there are pointers to the mMeterClass and mpMeterDraw structs + if (!dMeter2Info_getMeterClass()) + { + return false; + } + + if (!dMeter2Info_getMeterClass()->getMeterDrawPtr()) + { + return false; + } + + // Make sure Z button isn't dimmed + if (dMeter2Info_getMeterClass()->getMeterDrawPtr()->getZButtonAlpha() != 1.f) + { + return false; + } + + switch (daAlink_getAlinkActorClass()->mProcID) + { + case daAlink_c::PROC_TALK: + case daAlink_c::PROC_WOLF_SWIM_MOVE: + case daAlink_c::PROC_SWIM_MOVE: + case daAlink_c::PROC_SWIM_WAIT: + case daAlink_c::PROC_WOLF_SWIM_WAIT: + case daAlink_c::PROC_SWIM_UP: + case daAlink_c::PROC_SWIM_DIVE: + { + return false; + } + default: + { + break; + } + } + return true; +} + + RandomizerContext& randomizer_GetContext() { static RandomizerContext instance; return instance; @@ -218,6 +524,9 @@ u32 getActorCRC32(stage_actor_data_class* actor) { return zng_crc32(0, reinterpret_cast(actor), RandomizerContext::ACTOR_CRC_SIZE); } +/* + * Generates a seed and writes the necessary seed files to the players seed directory + */ void GenerateAndWriteSeed(std::string& generationStatusMsg) { const auto result = SDL_GetPrefPath(dusk::OrgName, dusk::AppName); if (!result) { @@ -254,6 +563,16 @@ void GenerateAndWriteSeed(std::string& generationStatusMsg) { randoData.mTreasureChestOverrides[stage][tboxId] = itemId; } + // Poe Overrides + // Keyed by u16 of 0xFF00 (stage index) and 0x00FF (collectible flag) + if (location->HasCategories("Poe")) { + const auto& stage = metaData[0]["Stage"].as(); + const auto& flag = metaData[0]["Flag"].as(); + u8 itemId = location->GetCurrentItem()->GetID(); + u16 key = (stage << 8) | flag; + randoData.mPoeOverrides[key] = itemId; + } + // Freestanding Overrides // Keyed by the stage index and collectible flag of the item if (location->HasCategories("Freestanding Item")) { diff --git a/src/dusk/randomizer/game/randomizer_context.hpp b/src/dusk/randomizer/game/randomizer_context.hpp index 5ecf1bcd36..f1c4d5a783 100644 --- a/src/dusk/randomizer/game/randomizer_context.hpp +++ b/src/dusk/randomizer/game/randomizer_context.hpp @@ -9,6 +9,10 @@ #include #include +/* + * Class holding all the information necessary for playing + * the current randomizer seed + */ class RandomizerContext { public: static constexpr size_t ACTOR_CRC_SIZE = 30; @@ -25,7 +29,7 @@ public: std::unordered_map> mStartRegionFlags{}; std::list mStartingInventory{}; std::unordered_map> mTreasureChestOverrides{}; - std::unordered_map> mPoeOverrides{}; + std::unordered_map mPoeOverrides{}; std::unordered_map> mFreestandingItemOverrides{}; std::unordered_map mBugRewardOverrides{}; std::unordered_map mSkyCharacterOverrides{}; @@ -40,6 +44,61 @@ public: std::string GetSeedDataPath() const; }; +/* + * Class holding seed-agnostic dynamic information about current randomizer play. + * This gets reset when reseting to the title screen. + */ +class RandomizerState { +public: + enum TimeChange { + NO_CHANGE = 0, + CHANGE_TO_NIGHT, + CHANGE_TO_DAY, + }; + + enum EventItemStatus { + QUEUE_EMPTY, + ITEM_IN_QUEUE, + CLEAR_QUEUE, + }; + + static constexpr u8 EVENT_ITEM_QUEUE_SIZE = 10; + + RandomizerState() {mInitialized = false;} + + int _create(); + int _delete(); + int execute(); + int draw(); + void addItemToEventQueue(u8 item); + void initGiveItemToPlayer(); + //void handleBonkDamage(); + void handleTimeOfDayChange(); + void handleTimeSpeed(); + void offLoad(); + + void handlePoeItem(u8 bitSw); + + u8 getGiveItemToPlayerStatus() const { return mEventItemStatus;} + u8 getTimeChange() const { return mTimeChange; } + bool getRoomReloadingState() const { return mRoomReloadingState; } + bool getHasPendingToDChange() const { return mHasPendingToDChange; } + + void setGiveItemToPlayerStatus(u8 status) { mEventItemStatus = status;} + void setHasPendingToDChange(bool hasPending) { mHasPendingToDChange = hasPending; } + void setTimeChange(u8 newTimeChange) { mTimeChange = newTimeChange; } + void setRoomReloadingState(bool newState) { mRoomReloadingState = newState; } + + bool mInitialized{false}; + u8 mEventItemStatus{}; + bool mHasPendingToDChange{false}; + u8 mTimeChange{}; + u8 mEventItemQueue[EVENT_ITEM_QUEUE_SIZE]; + bool mRoomReloadingState{false}; +}; + +extern RandomizerState g_randomizerState; + RandomizerContext& randomizer_GetContext(); bool randomizer_IsActive(); diff --git a/src/f_pc/f_pc_manager.cpp b/src/f_pc/f_pc_manager.cpp index ad226b4d1d..3cc923a492 100644 --- a/src/f_pc/f_pc_manager.cpp +++ b/src/f_pc/f_pc_manager.cpp @@ -23,6 +23,11 @@ #include "tracy/Tracy.hpp" +#if TARGET_PC +#include "dusk/randomizer/game/randomizer_context.hpp" +#endif + + void fpcM_Draw(void* i_proc) { fpcDw_Execute((base_process_class*)i_proc); } @@ -89,6 +94,16 @@ void fpcM_Management(fpcM_ManagementFunc i_preExecuteFn, fpcM_ManagementFunc i_p i_preExecuteFn(); } +#if TARGET_PC + if (randomizer_IsActive()) { + if (!g_randomizerState.mInitialized) { + g_randomizerState._create(); + } + g_randomizerState.execute(); + } else if (g_randomizerState.mInitialized) { + g_randomizerState = RandomizerState{}; + } +#endif if (!fapGm_HIO_c::isCaptureScreen()) { fpcEx_Handler((fpcLnIt_QueueFunc)fpcM_Execute); } @@ -97,6 +112,12 @@ void fpcM_Management(fpcM_ManagementFunc i_preExecuteFn, fpcM_ManagementFunc i_p fpcDw_Handler((fpcDw_HandlerFuncFunc)fpcM_DrawIterater, (fpcDw_HandlerFunc)fpcM_Draw); } +#if TARGET_PC + if (randomizer_IsActive()) { + g_randomizerState.draw(); + } +#endif + if (i_postExecuteFn != NULL) { i_postExecuteFn(); }