diff --git a/soh/include/z64actor.h b/soh/include/z64actor.h index 77172bda4aa..d4273bc5a2e 100644 --- a/soh/include/z64actor.h +++ b/soh/include/z64actor.h @@ -358,8 +358,6 @@ typedef enum { /* 0x19 */ ITEM00_BOMBS_SPECIAL, /* 0x1A */ ITEM00_BOMBCHU, /* 0x1B */ ITEM00_SOH_DUMMY, - /* 0x1C */ ITEM00_SOH_GIVE_ITEM_ENTRY, - /* 0x1D */ ITEM00_SOH_GIVE_ITEM_ENTRY_GI, /* 0x1E */ ITEM00_MAX, /* 0xFF */ ITEM00_NONE = 0xFF } Item00Type; diff --git a/soh/include/z64item.h b/soh/include/z64item.h index b1fa897fe6e..866c4e20d43 100644 --- a/soh/include/z64item.h +++ b/soh/include/z64item.h @@ -309,6 +309,7 @@ typedef enum { /* 0x99 */ ITEM_STICK_UPGRADE_30, /* 0x9A */ ITEM_NUT_UPGRADE_30, /* 0x9B */ ITEM_NUT_UPGRADE_40, + /* 0x9C */ ITEM_SHIP, // SOH [Enhancement] Added to enable custom item gives /* 0xFC */ ITEM_LAST_USED = 0xFC, /* 0xFE */ ITEM_NONE_FE = 0xFE, /* 0xFF */ ITEM_NONE = 0xFF @@ -460,7 +461,8 @@ typedef enum { /* 0x7B */ GI_BULLET_BAG_50, /* 0x7C */ GI_ICE_TRAP, // freezes link when opened from a chest /* 0x7D */ GI_TEXT_0, // no model appears over Link, shows text id 0 (pocket egg) - /* 0x84 */ GI_MAX + /* 0x7E */ GI_SHIP, // SOH [Enhancement] Added to enable custom item gives + /* 0x7E */ GI_MAX } GetItemID; typedef enum { diff --git a/soh/soh/Enhancements/custom-item/CustomItem.cpp b/soh/soh/Enhancements/custom-item/CustomItem.cpp new file mode 100644 index 00000000000..88ac485dc9f --- /dev/null +++ b/soh/soh/Enhancements/custom-item/CustomItem.cpp @@ -0,0 +1,260 @@ +#include "CustomItem.h" +#include "soh/Enhancements/game-interactor/GameInteractor.h" + +extern "C" { +#include "z64actor.h" +#include "functions.h" +#include "variables.h" +#include "macros.h" +extern PlayState* gPlayState; +} + +// #region These were copied from z_en_item00.c +static ColliderCylinderInit sCylinderInit = { + { + COLTYPE_NONE, + AT_NONE, + AC_ON | AT_TYPE_PLAYER, + OC1_NONE, + OC2_NONE, + COLSHAPE_CYLINDER, + }, + { + ELEMTYPE_UNK0, + { 0x00000000, 0x00, 0x00 }, + { 0x00000010, 0x00, 0x00 }, + TOUCH_NONE | TOUCH_SFX_NORMAL, + BUMP_ON, + OCELEM_NONE, + }, + { 10, 30, 0, { 0, 0, 0 } }, +}; + +static InitChainEntry sInitChain[] = { + ICHAIN_F32(targetArrowOffset, 2000, ICHAIN_STOP), +}; +// #endregion + +EnItem00* CustomItem::Spawn(f32 posX, f32 posY, f32 posZ, s16 rot, s16 flags, s16 params, ActorFunc actionFunc, + ActorFunc drawFunc) { + if (!gPlayState) { + return nullptr; + } + + Actor* actor = Actor_Spawn(&gPlayState->actorCtx, gPlayState, ACTOR_EN_ITEM00, posX, posY, posZ, flags, rot, params, + ITEM00_NONE, false); + EnItem00* enItem00 = (EnItem00*)actor; + + if (actionFunc != NULL) { + enItem00->actionFunc = (EnItem00ActionFunc)actionFunc; + } + + if (drawFunc != NULL) { + actor->draw = drawFunc; + } + + return enItem00; +} + +void CustomItem_Init(Actor* actor, PlayState* play) { + EnItem00* enItem00 = (EnItem00*)actor; + + if (CUSTOM_ITEM_FLAGS & CustomItem::STOP_BOBBING) { + actor->shape.yOffset = 1250.0f; + } else { + actor->shape.yOffset = (Math_SinS(actor->shape.rot.y) * 150.0f) + 1250.0f; + } + + if (CUSTOM_ITEM_FLAGS & CustomItem::HIDE_TILL_OVERHEAD) { + Actor_SetScale(actor, 0.0f); + } else { + Actor_SetScale(actor, 0.015f); + } + + if (CUSTOM_ITEM_FLAGS & CustomItem::KEEP_ON_PLAYER) { + Math_Vec3f_Copy(&actor->world.pos, &GET_PLAYER(play)->actor.world.pos); + } + + if (CUSTOM_ITEM_FLAGS & CustomItem::TOSS_ON_SPAWN) { + actor->velocity.y = 8.0f; + actor->speedXZ = 2.0f; + actor->gravity = -1.4f; + actor->world.rot.y = Rand_ZeroOne() * 40000.0f; + } + + Actor_ProcessInitChain(actor, sInitChain); + Collider_InitCylinder(play, &enItem00->collider); + Collider_SetCylinder(play, &enItem00->collider, actor, &sCylinderInit); + + enItem00->unk_15A = -1; +} + +// By default this will just assume the GID was passed in as the rot z, if you want different functionality you should +// override the draw +void CustomItem_Draw(Actor* actor, PlayState* play) { + Matrix_Scale(30.0f, 30.0f, 30.0f, MTXMODE_APPLY); + GetItem_Draw(play, CUSTOM_ITEM_PARAM); +} + +// Once the item is touched we need to clear movement vars so the item doesn't sink in the players hands/above head +void CustomItem_ItemTouched(Actor* actor, PlayState* play) { + actor->speedXZ = 0.0f; + actor->velocity.y = 0.0f; + actor->gravity = 0.0f; + actor->shape.yOffset = 1250.0f; +} + +void CustomItem_Update(Actor* actor, PlayState* play) { + EnItem00* enItem00 = (EnItem00*)actor; + Player* player = GET_PLAYER(play); + + if (!(CUSTOM_ITEM_FLAGS & CustomItem::STOP_SPINNING)) { + actor->shape.rot.y += 960; + } + + if (!(CUSTOM_ITEM_FLAGS & CustomItem::STOP_BOBBING)) { + actor->shape.yOffset = (Math_SinS(actor->shape.rot.y) * 150.0f) + 1250.0f; + } + + if (CUSTOM_ITEM_FLAGS & CustomItem::HIDE_TILL_OVERHEAD) { + Actor_SetScale(actor, 0.0f); + } + + if (CUSTOM_ITEM_FLAGS & CustomItem::KEEP_ON_PLAYER) { + Math_Vec3f_Copy(&actor->world.pos, &GET_PLAYER(play)->actor.world.pos); + } + + // Player range check accounting for goron rolling behavior. Matches EnItem00 range check. + bool playerInRangeOfPickup = (actor->xzDistToPlayer <= 30.0f) && (fabsf(actor->yDistToPlayer) <= fabsf(50.0f)); + + if (CUSTOM_ITEM_FLAGS & CustomItem::KILL_ON_TOUCH) { + // Pretty self explanatory, if the player is within range, kill the actor and call the action function + if (playerInRangeOfPickup) { + if (enItem00->actionFunc != NULL) { + enItem00->actionFunc(enItem00, play); + CUSTOM_ITEM_FLAGS |= CustomItem::CALLED_ACTION; + } + Actor_Kill(actor); + } + } else if (CUSTOM_ITEM_FLAGS & CustomItem::GIVE_OVERHEAD) { + // If the item hasn't been picked up (unk_15A == -1) and the player is within range + if (enItem00->unk_15A == -1 && playerInRangeOfPickup) { + // Fire the action function + if (enItem00->actionFunc != NULL) { + enItem00->actionFunc(enItem00, play); + CUSTOM_ITEM_FLAGS |= CustomItem::CALLED_ACTION; + } + Sfx_PlaySfxCentered(NA_SE_SY_GET_ITEM); + // Set the unk_15A to 15, this indicates the item has been picked up and will start the overhead animation + enItem00->unk_15A = 15; + CUSTOM_ITEM_FLAGS |= CustomItem::STOP_BOBBING; + CUSTOM_ITEM_FLAGS |= CustomItem::KEEP_ON_PLAYER; + CustomItem_ItemTouched(actor, play); + // Move to player right away on this frame + Math_Vec3f_Copy(&actor->world.pos, &GET_PLAYER(play)->actor.world.pos); + } + + // If the item has been picked up + if (enItem00->unk_15A > 0) { + // Reduce the size a bit, but also makes it visible for HIDE_TILL_OVERHEAD + Actor_SetScale(actor, 0.010f); + + // Decrement the unk_15A, which will be used to bob the item up and down + enItem00->unk_15A--; + + // Account for the different heights of the player forms + f32 height = LINK_IS_ADULT ? 60.0f : 45.0f; + + // Bob the item up and down + actor->world.pos.y += (height + (Math_SinS(enItem00->unk_15A * 15000) * (enItem00->unk_15A * 0.3f))); + } + + // Finally, once the bobbing animation is done, kill the actor + if (enItem00->unk_15A == 0) { + Actor_Kill(actor); + } + } else if (CUSTOM_ITEM_FLAGS & CustomItem::GIVE_ITEM_CUTSCENE) { + // If the item hasn't been picked up and the player is within range + + if (!Actor_HasParent(actor, play) && enItem00->unk_15A == -1) { + Actor_OfferGetItem(actor, play, GI_SHIP, 50.0f, 80.0f); + } else { + if (enItem00->unk_15A == -1) { + // actor->shape.yOffset = 1250.0f; + CUSTOM_ITEM_FLAGS |= CustomItem::STOP_BOBBING; + // Math_Vec3f_Copy(&actor->world.pos, &GET_PLAYER(play)->actor.world.pos); + CUSTOM_ITEM_FLAGS |= CustomItem::KEEP_ON_PLAYER; + // Actor_SetScale(actor, 0.0f); + CUSTOM_ITEM_FLAGS |= CustomItem::HIDE_TILL_OVERHEAD; + CustomItem_ItemTouched(actor, play); + } + + // Begin incrementing the unk_15A, indicating the item has been picked up + enItem00->unk_15A++; + + // For the first 20 frames, wait while the player's animation plays + if (enItem00->unk_15A >= 20) { + // After the first 20 frames, show the item and call the action function + if (enItem00->unk_15A == 20 && enItem00->actionFunc != NULL) { + enItem00->actionFunc(enItem00, play); + CUSTOM_ITEM_FLAGS |= CustomItem::CALLED_ACTION; + } + // Override the bobbing animation to be a fixed height + actor->shape.yOffset = 900.0f; + Actor_SetScale(actor, 0.007f); + + // Account for the different heights of the player forms + f32 height = LINK_IS_ADULT ? 60.0f : 45.0f; + + actor->world.pos.y += height; + } + + // Once the player is no longer in the "Give Item" state, kill the actor + if (!(player->stateFlags1 & PLAYER_STATE1_GETTING_ITEM)) { + Actor_Kill(actor); + } + } + } + + if (actor->gravity != 0.0f) { + Actor_MoveXZGravity(actor); + Actor_UpdateBgCheckInfo(play, actor, 20.0f, 15.0f, 15.0f, 0x1D); + } + + if (actor->bgCheckFlags & 0x0003) { + actor->speedXZ = 0.0f; + } + + if (CUSTOM_ITEM_FLAGS & CustomItem::ABLE_TO_BOOMERANG) { + Collider_UpdateCylinder(actor, &enItem00->collider); + CollisionCheck_SetAC(play, &play->colChkCtx, &enItem00->collider.base); + } +} + +void CustomItem_Destroy(Actor* actor, PlayState* play) { + EnItem00* enItem00 = (EnItem00*)actor; + + Collider_DestroyCylinder(play, &enItem00->collider); +} + +void CustomItem::RegisterHooks() { + GameInteractor::Instance->RegisterGameHookForID( + ACTOR_EN_ITEM00, [](void* actorRef, bool* should) { + Actor* actor = (Actor*)actorRef; + if (actor->params != ITEM00_NONE) { + return; + } + + actor->init = CustomItem_Init; + actor->update = CustomItem_Update; + actor->draw = CustomItem_Draw; + actor->destroy = CustomItem_Destroy; + + // Set the rotX/rotZ back to 0, the original values can be accessed from actor->home + actor->world.rot.x = 0; + actor->world.rot.z = 0; + actor->shape.rot.x = 0; + actor->shape.rot.y = 0; + actor->shape.rot.z = 0; + }); +} diff --git a/soh/soh/Enhancements/custom-item/CustomItem.h b/soh/soh/Enhancements/custom-item/CustomItem.h new file mode 100644 index 00000000000..e7be3f86aa4 --- /dev/null +++ b/soh/soh/Enhancements/custom-item/CustomItem.h @@ -0,0 +1,25 @@ +extern "C" { +#include "z64actor.h" +} + +#define CUSTOM_ITEM_FLAGS (actor->home.rot.x) +#define CUSTOM_ITEM_PARAM (actor->home.rot.z) + +namespace CustomItem { + +enum CustomItemFlags : int16_t { + KILL_ON_TOUCH = 1 << 0, // 0000 0000 0000 0001 + GIVE_OVERHEAD = 1 << 1, // 0000 0000 0000 0010 + GIVE_ITEM_CUTSCENE = 1 << 2, // 0000 0000 0000 0100 + HIDE_TILL_OVERHEAD = 1 << 3, // 0000 0000 0000 1000 + KEEP_ON_PLAYER = 1 << 4, // 0000 0000 0001 0000 + STOP_BOBBING = 1 << 5, // 0000 0000 0010 0000 + STOP_SPINNING = 1 << 6, // 0000 0000 0100 0000 + CALLED_ACTION = 1 << 7, // 0000 0000 1000 0000 + TOSS_ON_SPAWN = 1 << 8, // 0000 0001 0000 0000 + ABLE_TO_BOOMERANG = 1 << 9, // 0000 0010 0000 0000 +}; +void RegisterHooks(); +EnItem00* Spawn(f32 posX, f32 posY, f32 posZ, s16 rot, s16 flags, s16 params, ActorFunc actionFunc = NULL, + ActorFunc drawFunc = NULL); +}; // namespace CustomItem diff --git a/soh/soh/Enhancements/custom-message/CustomMessageManager.cpp b/soh/soh/Enhancements/custom-message/CustomMessageManager.cpp index 0ebba19ec26..14fe831df44 100644 --- a/soh/soh/Enhancements/custom-message/CustomMessageManager.cpp +++ b/soh/soh/Enhancements/custom-message/CustomMessageManager.cpp @@ -1,5 +1,7 @@ #include "CustomMessageManager.h" #include "CustomMessageInterfaceAddon.h" +#include "CustomMessageTypes.h" +#include "soh/Enhancements/game-interactor/GameInteractor.h" #include #include #include @@ -7,6 +9,14 @@ #include #include +#include "soh/util.h" + +extern "C" { +#include "functions.h" + +extern PlayState* gPlayState; +} + using namespace std::literals::string_literals; static const std::unordered_map textBoxSpecialCharacters = { @@ -212,6 +222,19 @@ const TextBoxPosition& CustomMessage::GetTextBoxPosition() const { return position; } +void CustomMessage::LoadIntoFont() { + MessageContext* msgCtx = &gPlayState->msgCtx; + Font* font = &msgCtx->font; + char* buffer = font->msgBuf; + const int maxBufferSize = sizeof(font->msgBuf); + + font->charTexBuf[0] = (type << 4) | position; + + std::string content = GetForCurrentLanguage(MF_RAW); + + msgCtx->msgLength = font->msgLength = SohUtils::CopyStringToCharBuffer(buffer, content, maxBufferSize); +} + CustomMessage CustomMessage::operator+(const CustomMessage& right) const { std::vector newColors = colors; std::vector rColors = right.GetColors(); @@ -761,6 +784,10 @@ std::string CustomMessage::PLAYER_NAME() { return "\x0F"s; } +std::string CustomMessage::SKULLS_DESTROYED() { + return "\x19"s; +} + bool CustomMessageManager::InsertCustomMessage(std::string tableID, uint16_t textID, CustomMessage messages) { auto foundMessageTable = messageTables.find(tableID); if (foundMessageTable == messageTables.end()) { @@ -821,3 +848,21 @@ bool CustomMessageManager::AddCustomMessageTable(std::string tableID) { CustomMessageTable newMessageTable; return messageTables.emplace(tableID, newMessageTable).second; } + +void CustomMessageManager::SetActiveCustomMessage(CustomMessage message) { + activeCustomMessage = message; +} + +void CustomMessageManager::StartTextbox(CustomMessage message) { + activeCustomMessage = message; + + Message_StartTextbox(gPlayState, TEXT_CUSTOM_MESSAGE, &GET_PLAYER(gPlayState)->actor); +} + +void CustomMessageManager::RegisterHooks() { + GameInteractor::Instance->RegisterGameHookForID( + TEXT_CUSTOM_MESSAGE, [&](u16* textId, bool* loadFromMessageTable) { + *loadFromMessageTable = false; + activeCustomMessage.LoadIntoFont(); + }); +} diff --git a/soh/soh/Enhancements/custom-message/CustomMessageManager.h b/soh/soh/Enhancements/custom-message/CustomMessageManager.h index 43b89847645..966e6b9cab2 100644 --- a/soh/soh/Enhancements/custom-message/CustomMessageManager.h +++ b/soh/soh/Enhancements/custom-message/CustomMessageManager.h @@ -61,6 +61,7 @@ class CustomMessage { static std::string POINTS(std::string x); // HIGH_SCORE is also a macro static std::string WAIT_FOR_INPUT(); static std::string PLAYER_NAME(); + static std::string SKULLS_DESTROYED(); const std::string GetEnglish(MessageFormat format = MF_FORMATTED) const; const std::string GetFrench(MessageFormat format = MF_FORMATTED) const; @@ -77,6 +78,9 @@ class CustomMessage { void SetTextBoxType(TextBoxType boxType); const TextBoxPosition& GetTextBoxPosition() const; + // To only be used with OnOpenText hook + void LoadIntoFont(); + CustomMessage operator+(const CustomMessage& right) const; CustomMessage operator+(const std::string& right) const; void operator+=(const std::string& right); @@ -244,11 +248,15 @@ class CustomMessageManager { bool InsertCustomMessage(std::string tableID, uint16_t textID, CustomMessage message); + CustomMessage activeCustomMessage; + public: static CustomMessageManager* Instance; CustomMessageManager() = default; + void RegisterHooks(); + /** * @brief Formats the provided Custom Message Entry and inserts it into the table with the provided tableID, * with the provided giid (getItemID) as its key. This function also inserts the icon corresponding to @@ -307,6 +315,22 @@ class CustomMessageManager { * already exists.) */ bool AddCustomMessageTable(std::string tableID); + + /** + * @brief Sets the active custom message, which will be used the next time + * TEXT_CUSTOM_MESSAGE is used for a text box. + * + * @param message the message to set as active + */ + void SetActiveCustomMessage(CustomMessage message); + + /** + * @brief Displays a custom message in a textbox. This is the same as calling + * SetActiveCustomMessage and then beginning a textbox with TEXT_CUSTOM_MESSAGE. + * + * @param message the message to set as active + */ + void StartTextbox(CustomMessage message); }; class MessageNotFoundException : public std::exception { diff --git a/soh/soh/Enhancements/custom-message/CustomMessageTypes.h b/soh/soh/Enhancements/custom-message/CustomMessageTypes.h index 86d0d485666..16bb1961fad 100644 --- a/soh/soh/Enhancements/custom-message/CustomMessageTypes.h +++ b/soh/soh/Enhancements/custom-message/CustomMessageTypes.h @@ -31,6 +31,7 @@ typedef enum { TEXT_PURPLE_RUPEE = 0x00F1, TEXT_HUGE_RUPEE = 0x00F2, TEXT_RANDOMIZER_CUSTOM_ITEM = 0x00F8, + TEXT_CUSTOM_MESSAGE = 0x0109, TEXT_NAVI_DEKU_TREE_SUMMONS = 0x0140, TEXT_NAVI_CMON_BE_BRAVE = 0x0141, TEXT_NAVI_VISIT_THE_PRINCESS = 0x0142, diff --git a/soh/soh/Enhancements/game-interactor/GIEventQueue.cpp b/soh/soh/Enhancements/game-interactor/GIEventQueue.cpp new file mode 100644 index 00000000000..7e1e05583a6 --- /dev/null +++ b/soh/soh/Enhancements/game-interactor/GIEventQueue.cpp @@ -0,0 +1,99 @@ +#include "GameInteractor.h" +#include "soh/Enhancements/custom-item/CustomItem.h" + +extern "C" { +#include "variables.h" +#include "macros.h" +#include "functions.h" + +extern SaveContext gSaveContext; +extern PlayState* gPlayState; +} + +void ProcessEvents() { + Player* player = GET_PLAYER(gPlayState); + + // If the player has a message active, stop + if (gPlayState->msgCtx.msgMode != 0) { + return; + } + + // If the player is in a blocking cutscene, stop + if (Player_InBlockingCsMode(gPlayState, player)) { + return; + } + + // If player is dead, stop + if (player->stateFlags1 & PLAYER_STATE1_DEAD) { + return; + } + + // If there is an event active, stop + const auto& currentEvent = GameInteractor::Instance->currentEvent; + if (auto e = std::get_if(¤tEvent)) { + // no-op + } else { + return; + } + + // If there are no events that need to happen, stop + if (GameInteractor::Instance->events.empty()) { + return; + } + + GameInteractor::Instance->currentEvent = GameInteractor::Instance->events.front(); + const auto& nextEvent = GameInteractor::Instance->currentEvent; + + if (auto e = std::get_if(&nextEvent)) { + EnItem00* enItem00; + + s16 flags = CustomItem::HIDE_TILL_OVERHEAD | CustomItem::KEEP_ON_PLAYER; + + // If the player is climbing or in the air, deliver the item without a cutscene but freeze the player + if (!e->showGetItemCutscene || + (player->stateFlags1 & + (PLAYER_STATE1_CHARGING_SPIN_ATTACK | PLAYER_STATE1_HANGING_OFF_LEDGE | PLAYER_STATE1_CLIMBING_LEDGE | + PLAYER_STATE1_JUMPING | PLAYER_STATE1_FREEFALL | PLAYER_STATE1_FIRST_PERSON | + PLAYER_STATE1_CARRYING_ACTOR | PLAYER_STATE1_CLIMBING_LADDER | PLAYER_STATE1_IN_WATER)) || + (Player_GetExplosiveHeld(player) > -1)) { + flags |= CustomItem::GIVE_OVERHEAD; + } else { + flags |= CustomItem::GIVE_ITEM_CUTSCENE; + } + enItem00 = CustomItem::Spawn( + player->actor.world.pos.x, player->actor.world.pos.y, player->actor.world.pos.z, 0, flags, e->param, + [](Actor* actor, PlayState* play) { + Player* player = GET_PLAYER(gPlayState); + const auto& nextEvent = GameInteractor::Instance->currentEvent; + if (auto e = std::get_if(&nextEvent)) { + e->giveItem(actor, play); + if (e->showGetItemCutscene && !(CUSTOM_ITEM_FLAGS & CustomItem::GIVE_ITEM_CUTSCENE)) { + player->actor.freezeTimer = 30; + } + GameInteractor::Instance->currentEvent = GIEventNone{}; + } + }, + e->drawItem); + enItem00->actor.destroy = [](Actor* actor, PlayState* play) { + if (!(CUSTOM_ITEM_FLAGS & CustomItem::CALLED_ACTION)) { + // Event was not handled, requeue it + GameInteractor::Instance->events.push_back(GameInteractor::Instance->currentEvent); + } + }; + } + + GameInteractor::Instance->events.erase(GameInteractor::Instance->events.begin()); +} + +void GameInteractor::RegisterOwnHooks() { + // Cleanup all hooks at the start of each frame + GameInteractor::Instance->RegisterGameHook( + []() { GameInteractor::Instance->RemoveAllQueuedHooks(); }); + + COND_HOOK(OnLoadGame, true, [](int32_t fileNum) { + GameInteractor::Instance->currentEvent = GIEventNone{}; + GameInteractor::Instance->events.clear(); + }); + + GameInteractor::Instance->RegisterGameHook(ProcessEvents); +} diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor.h b/soh/soh/Enhancements/game-interactor/GameInteractor.h index 8ac2d9dda39..b60ef88ef9e 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor.h +++ b/soh/soh/Enhancements/game-interactor/GameInteractor.h @@ -108,6 +108,22 @@ void GameInteractor_SetTriforceHuntCreditsWarpActive(uint8_t state); #pragma message("Compiling without support, the Hook Debugger will not be available") #endif +struct GIEventNone {}; + +struct GIEventGiveItem { + // Whether or not to show the get item cutscene. If true and the player is in the air, the + // player will instead be frozen for a few seconds. If this is true you _must_ call + // CustomMessage::SetActiveCustomMessage in the giveItem function otherwise you'll just see a blank message. + bool showGetItemCutscene; + // Arbitrary s16 that can be accessed from within the give/draw functions with CUSTOM_ITEM_PARAM + s16 param; + // These are run in the context of an item00 actor. This isn't super important but can be useful in some cases + ActorFunc giveItem; + ActorFunc drawItem; +}; + +typedef std::variant GIEvent; + typedef uint32_t HOOK_ID; enum HookType { @@ -193,6 +209,8 @@ class GameInteractor { public: static GameInteractor* Instance; + void RegisterOwnHooks(); + // Game State class State { public: @@ -225,6 +243,10 @@ class GameInteractor { static GameInteractionEffectQueryResult ApplyEffect(GameInteractionEffectBase* effect); static GameInteractionEffectQueryResult RemoveEffect(RemovableGameInteractionEffect* effect); + // EventQueue + std::vector events = {}; + GIEvent currentEvent = GIEventNone(); + // Game Hooks HOOK_ID nextHookId = 1; diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h b/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h index 5da302fe820..e5cd8717e59 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h @@ -50,6 +50,7 @@ DEFINE_HOOK(OnPlayerShieldControl, (float_t * sp50, float_t* sp54)); DEFINE_HOOK(OnPlayDestroy, ()); DEFINE_HOOK(OnPlayDrawBegin, ()); DEFINE_HOOK(OnPlayDrawEnd, ()); +DEFINE_HOOK(OnOpenText, (u16 * textId, bool* loadFromMessageTable)); DEFINE_HOOK(OnVanillaBehavior, (GIVanillaBehavior flag, bool* result, va_list originalArgs)); DEFINE_HOOK(OnSaveFile, (int32_t fileNum, int32_t sectionID)); DEFINE_HOOK(OnLoadFile, (int32_t fileNum)); diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp index 2ffb7099eba..a29778f0a8c 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp @@ -19,9 +19,6 @@ void GameInteractor_ExecuteOnExitGame(int32_t fileNum) { } void GameInteractor_ExecuteOnGameStateMainStart() { - // Cleanup all hooks at the start of each frame - GameInteractor::Instance->RemoveAllQueuedHooks(); - GameInteractor::Instance->ExecuteHooks(); } @@ -229,6 +226,12 @@ void GameInteractor_ExecuteOnPlayDrawEnd() { GameInteractor::Instance->ExecuteHooks(); } +void GameInteractor_ExecuteOnOpenText(u16* textId, bool* loadFromMessageTable) { + GameInteractor::Instance->ExecuteHooks(textId, loadFromMessageTable); + GameInteractor::Instance->ExecuteHooksForID(*textId, textId, loadFromMessageTable); + GameInteractor::Instance->ExecuteHooksForFilter(textId, loadFromMessageTable); +} + bool GameInteractor_Should(GIVanillaBehavior flag, u32 result, ...) { // Only the external function can use the Variadic Function syntax // To pass the va args to the next caller must be done using va_list and reading the args into it diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h index fe3533f7376..126cd7fcd24 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h @@ -53,6 +53,7 @@ void GameInteractor_ExecuteOnDungeonKeyUsedHooks(uint16_t mapIndex); void GameInteractor_ExecuteOnPlayDestroy(); void GameInteractor_ExecuteOnPlayDrawBegin(); void GameInteractor_ExecuteOnPlayDrawEnd(); +void GameInteractor_ExecuteOnOpenText(u16* textId, bool* loadFromMessageTable); bool GameInteractor_Should(GIVanillaBehavior flag, uint32_t result, ...); // MARK: - Save Files diff --git a/soh/soh/Enhancements/randomizer/hook_handlers.cpp b/soh/soh/Enhancements/randomizer/hook_handlers.cpp index fcadbb534d0..5c2fe4f286f 100644 --- a/soh/soh/Enhancements/randomizer/hook_handlers.cpp +++ b/soh/soh/Enhancements/randomizer/hook_handlers.cpp @@ -13,6 +13,8 @@ #include "soh/SohGui/ImGuiUtils.h" #include "soh/Notification/Notification.h" #include "soh/SaveManager.h" +#include "soh/Enhancements/custom-item/CustomItem.h" +#include "soh/frame_interpolation.h" extern "C" { #include "macros.h" @@ -56,6 +58,7 @@ extern "C" { #include "src/overlays/actors/ovl_Fishing/z_fishing.h" #include "src/overlays/actors/ovl_En_Mk/z_en_mk.h" #include "draw.h" +#include extern SaveContext gSaveContext; extern PlayState* gPlayState; @@ -70,6 +73,7 @@ extern void EnGe1_Wait_Archery(EnGe1* enGe1, PlayState* play); extern void EnGe1_SetAnimationIdle(EnGe1* enGe1); extern void EnGe1_SetAnimationIdle(EnGe1* enGe1); extern void EnGe2_SetupCapturePlayer(EnGe2* enGe2, PlayState* play); +extern void Player_DrawGetItemIceTrap(PlayState* play); } bool LocMatchesQuest(Rando::Location loc) { @@ -215,10 +219,240 @@ bool MeetsRainbowBridgeRequirements() { return false; } -// Todo Move this to randomizer context, clear it out on save load etc -static std::queue randomizerQueuedChecks; -static RandomizerCheck randomizerQueuedCheck = RC_UNKNOWN_CHECK; -static GetItemEntry randomizerQueuedItemEntry = GET_ITEM_NONE; +bool ShouldShowGetItemCutscene(RandomizerCheck rc, GetItemEntry getItemEntry) { + // Skipping ItemGet animation incompatible with checks that require closing a text box to finish + if (rc == RC_HF_OCARINA_OF_TIME_ITEM || rc == RC_SPIRIT_TEMPLE_SILVER_GAUNTLETS_CHEST || + rc == RC_MARKET_BOMBCHU_BOWLING_FIRST_PRIZE || rc == RC_MARKET_BOMBCHU_BOWLING_SECOND_PRIZE) { + return true; + } + + if (CVarGetInteger(CVAR_RANDOMIZER_ENHANCEMENT("TimeSavers.SkipGetItemAnimation"), SGIA_JUNK) == SGIA_DISABLED) { + return true; + } + + // If a mix of MQ/Vanilla, and item is a map, show animation for map hints + if ((getItemEntry.getItemId >= RG_DEKU_TREE_MAP && getItemEntry.getItemId <= RG_ICE_CAVERN_MAP && + getItemEntry.modIndex == MOD_RANDOMIZER) && + OTRGlobals::Instance->gRandomizer->GetRandoSettingValue(RSK_MQ_DUNGEON_RANDOM) != RO_MQ_DUNGEONS_NONE && + OTRGlobals::Instance->gRandomizer->GetRandoSettingValue(RSK_MQ_DUNGEON_COUNT) != 12) { + return true; + } + + // Treat small keys as junk if Skeleton Key is obtained. + if (getItemEntry.getItemCategory == ITEM_CATEGORY_SMALL_KEY && Flags_GetRandomizerInf(RAND_INF_HAS_SKELETON_KEY)) { + return true; + } + + if (CVarGetInteger(CVAR_RANDOMIZER_ENHANCEMENT("TimeSavers.SkipGetItemAnimation"), SGIA_JUNK) == SGIA_JUNK && + ((getItemEntry.getItemCategory != ITEM_CATEGORY_JUNK && + getItemEntry.getItemCategory != ITEM_CATEGORY_SKULLTULA_TOKEN && + getItemEntry.getItemCategory != ITEM_CATEGORY_LESSER))) { + return true; + } + + return false; +} + +void RandomizerQueueCheck(RandomizerCheck rc) { + SPDLOG_INFO("RandomizerQueueCheck: {}", static_cast(rc)); + + auto loc = Rando::Context::GetInstance()->GetItemLocation(rc); + if (loc == nullptr) { + SPDLOG_ERROR("RandomizerQueueCheck: Location not found for RC {}", static_cast(rc)); + return; + } + + if (loc->HasObtained()) { + SPDLOG_ERROR("RandomizerQueueCheck: Location already obtained for RC {}", static_cast(rc)); + return; + } + + RandomizerGet vanillaRandomizerGet = Rando::StaticData::GetLocation(rc)->GetVanillaItem(); + GetItemID vanillaGetItemId = (GetItemID)Rando::StaticData::RetrieveItem(vanillaRandomizerGet).GetItemID(); + GetItemEntry getItemEntry = Rando::Context::GetInstance()->GetFinalGIEntry(rc, true, vanillaGetItemId); + + // Reset ice trap scale in case it's an ice trap + iceTrapScale = 0.0f; + + GameInteractor::Instance->events.emplace_back(GIEventGiveItem{ + .showGetItemCutscene = ShouldShowGetItemCutscene(rc, getItemEntry), + .param = (int16_t)rc, + .giveItem = + [](Actor* actor, PlayState* play) { + RandomizerCheck rc = (RandomizerCheck)CUSTOM_ITEM_PARAM; + auto loc = Rando::Context::GetInstance()->GetItemLocation(rc); + RandomizerGet vanillaRandomizerGet = Rando::StaticData::GetLocation(rc)->GetVanillaItem(); + GetItemID vanillaGetItemId = + (GetItemID)Rando::StaticData::RetrieveItem(vanillaRandomizerGet).GetItemID(); + GetItemEntry getItemEntry = Rando::Context::GetInstance()->GetFinalGIEntry(rc, true, vanillaGetItemId); + + std::string prefix = "You found "; + switch (gSaveContext.language) { + case LANGUAGE_FRA: + prefix = "Vous obtenez: "; + break; + case LANGUAGE_GER: + prefix = "Du erhältst: "; + break; + default: + break; + } + + std::string itemName = getItemEntry.modIndex == MOD_NONE + ? SohUtils::GetItemName(getItemEntry.itemId) + : Rando::StaticData::RetrieveItem((RandomizerGet)getItemEntry.getItemId) + .GetName() + .GetForLanguage(gSaveContext.language); + + CustomMessage message = prefix + "%g" + itemName + "%w!"; + message.SetTextBoxType(TextBoxType::TEXTBOX_TYPE_BLUE); + const char* itemIcon = + getItemEntry.modIndex == MOD_NONE ? GetTextureForItemId(getItemEntry.itemId) : nullptr; + + if (getItemEntry.modIndex == MOD_RANDOMIZER && getItemEntry.itemId == RG_ICE_TRAP) { + message = Randomizer::GetIceTrapMessage(); + } else if (getItemEntry.modIndex == MOD_RANDOMIZER && getItemEntry.getItemId == RG_TRIFORCE_PIECE) { + message = Randomizer::GetTriforcePieceMessage(); + } else if (getItemEntry.modIndex == MOD_RANDOMIZER) { + message = CustomMessageManager::Instance->RetrieveMessage(Randomizer::getItemMessageTableID, + getItemEntry.getItemId); + } else if (getItemEntry.modIndex == MOD_NONE && getItemEntry.itemId == ITEM_SKULL_TOKEN) { + s16 gsCount = gSaveContext.inventory.gsTokens + ((IS_RANDO) ? 1 : 0); + message = CustomMessageManager::Instance->RetrieveMessage(customMessageTableID, TEXT_GS_FREEZE, + MF_FORMATTED); + message.Replace("[[gsCount]]", std::to_string(gsCount)); + } else { + message = CustomMessage::LoadVanillaMessageTableEntry(getItemEntry.textId); + } + + if (CUSTOM_ITEM_FLAGS & CustomItem::GIVE_ITEM_CUTSCENE) { + // This first case is for if we are displaying a GI cutscene + message.AutoFormat(); + CustomMessageManager::Instance->SetActiveCustomMessage(message); + } else if (ShouldShowGetItemCutscene(rc, getItemEntry)) { + // This case is for if we intended to display a GI cutscene, but the player was busy, so we have to + // display a vanishing text box while the player is frozen for a short time. + message.Replace(CustomMessage::MESSAGE_END(), ""); + message += "\x11\x02\x10"; + message.AutoFormat(); + CustomMessageManager::Instance->StartTextbox(message); + } else { + if (getItemEntry.getItemCategory != ITEM_CATEGORY_JUNK) { + Notification::Emit({ + .itemIcon = itemIcon, + .message = prefix, + .suffix = itemName, + }); + } + } + + if (getItemEntry.modIndex == MOD_NONE) { + // Things that should have been handled by the game but nintendo + switch (getItemEntry.itemId) { + case ITEM_SWORD_BGS: + gSaveContext.bgsFlag = true; + gSaveContext.swordHealth = 8; + break; + case ITEM_HEART_PIECE: + case ITEM_HEART_PIECE_2: + case ITEM_HEART_CONTAINER: + gSaveContext.healthAccumulator = 0x140; // Refill 20 hearts + if ((s32)(gSaveContext.inventory.questItems & 0xF0000000) == 0x40000000) { + gSaveContext.inventory.questItems ^= 0x40000000; + gSaveContext.healthCapacity += 0x10; + gSaveContext.health += 0x10; + } + break; + } + Item_Give(play, getItemEntry.itemId); + } else { + if (getItemEntry.getItemId == RG_ICE_TRAP) { + gSaveContext.ship.pendingIceTrapCount++; + } else { + Randomizer_Item_Give(play, getItemEntry); + } + } + + // This is typically called when you close the text box after getting an item, in case a previous + // function hid the interface. + gSaveContext.unk_13EA = 0; + Interface_ChangeAlpha(0x32); + + loc->SetCheckStatus(RCSHOW_COLLECTED); + CheckTracker::SpoilAreaFromCheck(rc); + CheckTracker::RecalculateAllAreaTotals(); + CheckTracker::RecalculateAvailableChecks(); + SaveManager::Instance->SaveSection(gSaveContext.fileNum, SECTION_ID_TRACKER_DATA, true); + + // Transition the player into the Naburu cutscene if we are at the appropriate spot and story cutscenes + // are not disabled. + if (rc == RC_SPIRIT_TEMPLE_SILVER_GAUNTLETS_CHEST && + !CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.Story"), IS_RANDO)) { + static uint32_t updateHook; + updateHook = GameInteractor::Instance->RegisterGameHook([]() { + Player* player = GET_PLAYER(gPlayState); + if (player == NULL || Player_InBlockingCsMode(gPlayState, player) || + player->stateFlags1 & PLAYER_STATE1_IN_ITEM_CS || + player->stateFlags1 & PLAYER_STATE1_GETTING_ITEM || + player->stateFlags1 & PLAYER_STATE1_CARRYING_ACTOR) { + return; + } + + gPlayState->nextEntranceIndex = ENTR_DESERT_COLOSSUS_EAST_EXIT; + gPlayState->transitionTrigger = TRANS_TRIGGER_START; + gSaveContext.nextCutsceneIndex = 0xFFF1; + gPlayState->transitionType = TRANS_TYPE_SANDSTORM_END; + GET_PLAYER(gPlayState)->stateFlags1 &= ~PLAYER_STATE1_IN_CUTSCENE; + Player_TryCsAction(gPlayState, NULL, 8); + GameInteractor::Instance->UnregisterGameHook(updateHook); + }); + } + + ((EnItem00*)actor)->itemEntry = getItemEntry; + }, + .drawItem = + [](Actor* actor, PlayState* play) { + GetItemEntry getItemEntry; + + if (CUSTOM_ITEM_FLAGS & CustomItem::CALLED_ACTION) { + getItemEntry = ((EnItem00*)actor)->itemEntry; + } else { + RandomizerCheck rc = (RandomizerCheck)CUSTOM_ITEM_PARAM; + auto loc = Rando::Context::GetInstance()->GetItemLocation(rc); + RandomizerGet vanillaRandomizerGet = Rando::StaticData::GetLocation(rc)->GetVanillaItem(); + GetItemID vanillaGetItemId = + (GetItemID)Rando::StaticData::RetrieveItem(vanillaRandomizerGet).GetItemID(); + getItemEntry = Rando::Context::GetInstance()->GetFinalGIEntry(rc, true, vanillaGetItemId); + } + + Matrix_Scale(30.0f, 30.0f, 30.0f, MTXMODE_APPLY); + func_8002EBCC(actor, play, 0); + func_8002ED80(actor, play, 0); + + if (CUSTOM_ITEM_FLAGS & CustomItem::CALLED_ACTION && getItemEntry.modIndex == MOD_RANDOMIZER && + getItemEntry.getItemId == RG_ICE_TRAP) { + if (CUSTOM_ITEM_FLAGS & CustomItem::GIVE_OVERHEAD) { + iceTrapScale = 0.8f; + } else { + if (iceTrapScale < 0.01) { + iceTrapScale += 0.001f; + } else if (iceTrapScale < 0.8f) { + iceTrapScale += 0.2f; + } + } + Player_DrawGetItemIceTrap(play); + } + + EnItem00_CustomItemsParticles(actor, play, getItemEntry); + + if (getItemEntry.modIndex == MOD_RANDOMIZER && getItemEntry.getItemId == RG_TRIFORCE_PIECE) { + Randomizer_DrawTriforcePieceGI(play, getItemEntry); + } else { + GetItemEntry_Draw(play, getItemEntry); + } + }, + }); +} void RandomizerOnFlagSetHandler(int16_t flagType, int16_t flag) { // Consume adult trade items @@ -259,8 +493,7 @@ void RandomizerOnFlagSetHandler(int16_t flagType, int16_t flag) { return; } - SPDLOG_INFO("Queuing RC: {}", static_cast(rc)); - randomizerQueuedChecks.push(rc); + RandomizerQueueCheck(rc); } void RandomizerOnSceneFlagSetHandler(int16_t sceneNum, int16_t flagType, int16_t flag) { @@ -285,140 +518,7 @@ void RandomizerOnSceneFlagSetHandler(int16_t sceneNum, int16_t flagType, int16_t if (loc == nullptr || loc->HasObtained() || loc->GetPlacedRandomizerGet() == RG_NONE) return; - SPDLOG_INFO("Queuing RC: {}", static_cast(rc)); - randomizerQueuedChecks.push(rc); -} - -static Vec3f spawnPos = { 0.0f, -999.0f, 0.0f }; - -void RandomizerOnPlayerUpdateForRCQueueHandler() { - // If we're already queued, don't queue again - if (randomizerQueuedCheck != RC_UNKNOWN_CHECK) - return; - - // If there's nothing to queue, don't queue - if (randomizerQueuedChecks.size() < 1) - return; - - // If we're in a cutscene, don't queue - Player* player = GET_PLAYER(gPlayState); - if (Player_InBlockingCsMode(gPlayState, player) || player->stateFlags1 & PLAYER_STATE1_IN_ITEM_CS || - player->stateFlags1 & PLAYER_STATE1_GETTING_ITEM || player->stateFlags1 & PLAYER_STATE1_CARRYING_ACTOR) { - return; - } - - RandomizerCheck rc = randomizerQueuedChecks.front(); - auto loc = Rando::Context::GetInstance()->GetItemLocation(rc); - RandomizerGet vanillaRandomizerGet = Rando::StaticData::GetLocation(rc)->GetVanillaItem(); - GetItemID vanillaItem = (GetItemID)Rando::StaticData::RetrieveItem(vanillaRandomizerGet).GetItemID(); - GetItemEntry getItemEntry = - Rando::Context::GetInstance()->GetFinalGIEntry(rc, true, (GetItemID)vanillaRandomizerGet); - - if (loc->HasObtained()) { - SPDLOG_INFO("RC {} already obtained, skipping", static_cast(rc)); - } else { - iceTrapScale = 0.0f; - randomizerQueuedCheck = rc; - randomizerQueuedItemEntry = getItemEntry; - SPDLOG_INFO("Queueing Item mod {} item {} from RC {}", getItemEntry.modIndex, getItemEntry.itemId, - static_cast(rc)); - if ( - // Skipping ItemGet animation incompatible with checks that require closing a text box to finish - rc != RC_HF_OCARINA_OF_TIME_ITEM && rc != RC_SPIRIT_TEMPLE_SILVER_GAUNTLETS_CHEST && - rc != RC_MARKET_BOMBCHU_BOWLING_FIRST_PRIZE && rc != RC_MARKET_BOMBCHU_BOWLING_SECOND_PRIZE && - // Always show ItemGet animation for ice traps - !(getItemEntry.modIndex == MOD_RANDOMIZER && getItemEntry.getItemId == RG_ICE_TRAP) && - // Always show ItemGet animation outside of randomizer to keep behaviour consistent in vanilla - IS_RANDO && - (CVarGetInteger(CVAR_RANDOMIZER_ENHANCEMENT("TimeSavers.SkipGetItemAnimation"), SGIA_JUNK) == SGIA_ALL || - (CVarGetInteger(CVAR_RANDOMIZER_ENHANCEMENT("TimeSavers.SkipGetItemAnimation"), SGIA_JUNK) == SGIA_JUNK && - ( - // crude fix to ensure map hints are readable. Ideally replace with better hint tracking. - !(getItemEntry.getItemId >= RG_DEKU_TREE_MAP && getItemEntry.getItemId <= RG_ICE_CAVERN_MAP && - getItemEntry.modIndex == MOD_RANDOMIZER) && - (getItemEntry.getItemCategory == ITEM_CATEGORY_JUNK || - getItemEntry.getItemCategory == ITEM_CATEGORY_SKULLTULA_TOKEN || - getItemEntry.getItemCategory == ITEM_CATEGORY_LESSER || - // Treat small keys as junk if Skeleton Key is obtained. - (getItemEntry.getItemCategory == ITEM_CATEGORY_SMALL_KEY && - Flags_GetRandomizerInf(RAND_INF_HAS_SKELETON_KEY))))))) { - Item_DropCollectible(gPlayState, &spawnPos, static_cast(ITEM00_SOH_GIVE_ITEM_ENTRY | 0x8000)); - } - } - - randomizerQueuedChecks.pop(); -} - -void RandomizerOnPlayerUpdateForItemQueueHandler() { - if (randomizerQueuedCheck == RC_UNKNOWN_CHECK) - return; - - Player* player = GET_PLAYER(gPlayState); - if (player == NULL || Player_InBlockingCsMode(gPlayState, player) || - player->stateFlags1 & PLAYER_STATE1_IN_ITEM_CS || player->stateFlags1 & PLAYER_STATE1_GETTING_ITEM || - player->stateFlags1 & PLAYER_STATE1_CARRYING_ACTOR) { - return; - } - - SPDLOG_INFO("Attempting to give Item mod {} item {} from RC {}", randomizerQueuedItemEntry.modIndex, - randomizerQueuedItemEntry.itemId, static_cast(randomizerQueuedCheck)); - GiveItemEntryWithoutActor(gPlayState, randomizerQueuedItemEntry); - if (player->stateFlags1 & PLAYER_STATE1_IN_WATER) { - // Allow the player to receive the item while swimming - player->stateFlags2 |= PLAYER_STATE2_UNDERWATER; - Player_ActionHandler_2(player, gPlayState); - } -} - -void RandomizerOnItemReceiveHandler(GetItemEntry receivedItemEntry) { - if (randomizerQueuedCheck == RC_UNKNOWN_CHECK) - return; - - auto loc = Rando::Context::GetInstance()->GetItemLocation(randomizerQueuedCheck); - if (randomizerQueuedItemEntry.modIndex == receivedItemEntry.modIndex && - randomizerQueuedItemEntry.itemId == receivedItemEntry.itemId) { - SPDLOG_INFO("Item received mod {} item {} from RC {}", receivedItemEntry.modIndex, receivedItemEntry.itemId, - static_cast(randomizerQueuedCheck)); - loc->SetCheckStatus(RCSHOW_COLLECTED); - CheckTracker::SpoilAreaFromCheck(randomizerQueuedCheck); - CheckTracker::RecalculateAllAreaTotals(); - CheckTracker::RecalculateAvailableChecks(); - SaveManager::Instance->SaveSection(gSaveContext.fileNum, SECTION_ID_TRACKER_DATA, true); - randomizerQueuedCheck = RC_UNKNOWN_CHECK; - randomizerQueuedItemEntry = GET_ITEM_NONE; - } - - if (receivedItemEntry.modIndex == MOD_NONE && - (receivedItemEntry.itemId == ITEM_HEART_PIECE || receivedItemEntry.itemId == ITEM_HEART_PIECE_2 || - receivedItemEntry.itemId == ITEM_HEART_CONTAINER)) { - gSaveContext.healthAccumulator = MAX_HEALTH; // Refill 20 hearts - if ((s32)(gSaveContext.inventory.questItems & 0xF0000000) == 0x40000000) { - gSaveContext.inventory.questItems ^= 0x40000000; - gSaveContext.healthCapacity += FULL_HEART_HEALTH; - gSaveContext.health += FULL_HEART_HEALTH; - } - } - - if (loc->GetRandomizerCheck() == RC_SPIRIT_TEMPLE_SILVER_GAUNTLETS_CHEST && - !CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.Story"), IS_RANDO)) { - static uint32_t updateHook; - updateHook = GameInteractor::Instance->RegisterGameHook([]() { - Player* player = GET_PLAYER(gPlayState); - if (player == NULL || Player_InBlockingCsMode(gPlayState, player) || - player->stateFlags1 & PLAYER_STATE1_IN_ITEM_CS || player->stateFlags1 & PLAYER_STATE1_GETTING_ITEM || - player->stateFlags1 & PLAYER_STATE1_CARRYING_ACTOR) { - return; - } - - gPlayState->nextEntranceIndex = ENTR_DESERT_COLOSSUS_EAST_EXIT; - gPlayState->transitionTrigger = TRANS_TRIGGER_START; - gSaveContext.nextCutsceneIndex = 0xFFF1; - gPlayState->transitionType = TRANS_TYPE_SANDSTORM_END; - GET_PLAYER(gPlayState)->stateFlags1 &= ~PLAYER_STATE1_IN_CUTSCENE; - Player_TryCsAction(gPlayState, NULL, 8); - GameInteractor::Instance->UnregisterGameHook(updateHook); - }); - } + RandomizerQueueCheck(rc); } void EnExItem_DrawRandomizedItem(EnExItem* enExItem, PlayState* play) { @@ -454,8 +554,7 @@ void EnItem00_DrawRandomizedItem(EnItem00* enItem00, PlayState* play) { f32 mtxScale = CVarGetFloat(CVAR_RANDOMIZER_ENHANCEMENT("TimeSavers.SkipGetItemAnimationScale"), 10.0f); Matrix_Scale(mtxScale, mtxScale, mtxScale, MTXMODE_APPLY); GetItemEntry randoItem = enItem00->itemEntry; - if (CVarGetInteger(CVAR_RANDOMIZER_ENHANCEMENT("MysteriousShuffle"), 0) && - enItem00->actor.params != ITEM00_SOH_GIVE_ITEM_ENTRY) { + if (CVarGetInteger(CVAR_RANDOMIZER_ENHANCEMENT("MysteriousShuffle"), 0)) { randoItem = GET_ITEM_MYSTERY; } func_8002EBCC(&enItem00->actor, play, 0); @@ -833,7 +932,7 @@ void RandomizerOnVanillaBehaviorHandler(GIVanillaBehavior id, bool* should, va_l *should = !Flags_GetTreasure(gPlayState, 0x1F); break; case VB_PLAY_NABOORU_CAPTURED_CS: - // This behavior is replicated for randomizer in RandomizerOnItemReceiveHandler + // This behavior is replicated for randomizer in RandomizerQueueCheck *should = false; break; case VB_SHIEK_PREPARE_TO_GIVE_SERENADE_OF_WATER: { @@ -916,11 +1015,6 @@ void RandomizerOnVanillaBehaviorHandler(GIVanillaBehavior id, bool* should, va_l item00->actor.draw = (ActorFunc)EnItem00_DrawRandomizedItem; *should = Rando::Context::GetInstance()->GetItemLocation(rc)->HasObtained(); } - } else if (item00->actor.params == ITEM00_SOH_GIVE_ITEM_ENTRY || - item00->actor.params == ITEM00_SOH_GIVE_ITEM_ENTRY_GI) { - GetItemEntry itemEntry = randomizerQueuedItemEntry; - item00->itemEntry = itemEntry; - item00->actor.draw = (ActorFunc)EnItem00_DrawRandomizedItem; } break; } @@ -1013,87 +1107,6 @@ void RandomizerOnVanillaBehaviorHandler(GIVanillaBehavior id, bool* should, va_l } Actor_Kill(&item00->actor); *should = false; - } else if (item00->actor.params == ITEM00_SOH_GIVE_ITEM_ENTRY) { - Audio_PlaySoundGeneral(NA_SE_SY_GET_ITEM, &gSfxDefaultPos, 4, &gSfxDefaultFreqAndVolScale, - &gSfxDefaultFreqAndVolScale, &gSfxDefaultReverb); - if (item00->itemEntry.modIndex == MOD_NONE) { - if (item00->itemEntry.getItemId == GI_SWORD_BGS) { - gSaveContext.bgsFlag = true; - } - Item_Give(gPlayState, static_cast(item00->itemEntry.itemId)); - } else if (item00->itemEntry.modIndex == MOD_RANDOMIZER) { - if (item00->itemEntry.getItemId == RG_ICE_TRAP) { - gSaveContext.ship.pendingIceTrapCount++; - } else { - Randomizer_Item_Give(gPlayState, item00->itemEntry); - } - } - - if (item00->itemEntry.modIndex == MOD_NONE) { - std::string message; - - switch (gSaveContext.language) { - case LANGUAGE_FRA: - message = "Vous obtenez: "; - break; - case LANGUAGE_GER: - message = "Du erhältst: "; - break; - case LANGUAGE_ENG: - default: - message = "You found "; - break; - } - - Notification::Emit({ - .itemIcon = GetTextureForItemId(item00->itemEntry.itemId), - .message = message, - .suffix = SohUtils::GetItemName(item00->itemEntry.itemId), - }); - } else if (item00->itemEntry.modIndex == MOD_RANDOMIZER) { - std::string message; - std::string itemName; - - switch (gSaveContext.language) { - case LANGUAGE_FRA: - message = "Vous obtenez: "; - itemName = Rando::StaticData::RetrieveItem((RandomizerGet)item00->itemEntry.getItemId) - .GetName() - .french; - break; - case LANGUAGE_GER: - message = "Du erhältst: "; - itemName = Rando::StaticData::RetrieveItem((RandomizerGet)item00->itemEntry.getItemId) - .GetName() - .german; - break; - case LANGUAGE_ENG: - default: - message = "You found "; - itemName = Rando::StaticData::RetrieveItem((RandomizerGet)item00->itemEntry.getItemId) - .GetName() - .english; - break; - } - - Notification::Emit({ - .message = message, - .suffix = itemName, - }); - } - - // This is typically called when you close the text box after getting an item, in case a previous - // function hid the interface. - gSaveContext.unk_13EA = 0; - Interface_ChangeAlpha(0x32); - // EnItem00_SetupAction(item00, func_8001E5C8); - // *should = false; - } else if (item00->actor.params == ITEM00_SOH_GIVE_ITEM_ENTRY_GI) { - if (!Actor_HasParent(&item00->actor, gPlayState)) { - GiveItemEntryFromActorWithFixedRange(&item00->actor, gPlayState, item00->itemEntry); - } - EnItem00_SetupAction(item00, func_8001E5C8); - *should = false; } break; } @@ -2421,9 +2434,6 @@ void RandomizerOnCuccoOrChickenHatch() { void RandomizerRegisterHooks() { static uint32_t onFlagSetHook = 0; static uint32_t onSceneFlagSetHook = 0; - static uint32_t onPlayerUpdateForRCQueueHook = 0; - static uint32_t onPlayerUpdateForItemQueueHook = 0; - static uint32_t onItemReceiveHook = 0; static uint32_t onDialogMessageHook = 0; static uint32_t onVanillaBehaviorHook = 0; static uint32_t onSceneInitHook = 0; @@ -2447,15 +2457,8 @@ void RandomizerRegisterHooks() { GameInteractor::Instance->RegisterGameHook([](int32_t fileNum) { ShipInit::Init("IS_RANDO"); - randomizerQueuedChecks = std::queue(); - randomizerQueuedCheck = RC_UNKNOWN_CHECK; - randomizerQueuedItemEntry = GET_ITEM_NONE; - GameInteractor::Instance->UnregisterGameHook(onFlagSetHook); GameInteractor::Instance->UnregisterGameHook(onSceneFlagSetHook); - GameInteractor::Instance->UnregisterGameHook(onPlayerUpdateForRCQueueHook); - GameInteractor::Instance->UnregisterGameHook(onPlayerUpdateForItemQueueHook); - GameInteractor::Instance->UnregisterGameHook(onItemReceiveHook); GameInteractor::Instance->UnregisterGameHook(onDialogMessageHook); GameInteractor::Instance->UnregisterGameHook(onVanillaBehaviorHook); GameInteractor::Instance->UnregisterGameHook(onSceneInitHook); @@ -2479,9 +2482,6 @@ void RandomizerRegisterHooks() { onFlagSetHook = 0; onSceneFlagSetHook = 0; - onPlayerUpdateForRCQueueHook = 0; - onPlayerUpdateForItemQueueHook = 0; - onItemReceiveHook = 0; onDialogMessageHook = 0; onVanillaBehaviorHook = 0; onSceneInitHook = 0; @@ -2518,12 +2518,6 @@ void RandomizerRegisterHooks() { GameInteractor::Instance->RegisterGameHook(RandomizerOnFlagSetHandler); onSceneFlagSetHook = GameInteractor::Instance->RegisterGameHook(RandomizerOnSceneFlagSetHandler); - onPlayerUpdateForRCQueueHook = GameInteractor::Instance->RegisterGameHook( - RandomizerOnPlayerUpdateForRCQueueHandler); - onPlayerUpdateForItemQueueHook = GameInteractor::Instance->RegisterGameHook( - RandomizerOnPlayerUpdateForItemQueueHandler); - onItemReceiveHook = - GameInteractor::Instance->RegisterGameHook(RandomizerOnItemReceiveHandler); onDialogMessageHook = GameInteractor::Instance->RegisterGameHook( RandomizerOnDialogMessageHandler); onVanillaBehaviorHook = GameInteractor::Instance->RegisterGameHook( diff --git a/soh/soh/OTRGlobals.cpp b/soh/soh/OTRGlobals.cpp index 2aadd55d207..33b354c9245 100644 --- a/soh/soh/OTRGlobals.cpp +++ b/soh/soh/OTRGlobals.cpp @@ -127,6 +127,7 @@ #include "soh/config/ConfigUpdaters.h" #include "soh/ShipInit.hpp" +#include "soh/Enhancements/custom-item/CustomItem.h" extern "C" { #include "src/overlays/actors/ovl_En_Dns/z_en_dns.h" @@ -769,6 +770,7 @@ extern "C" void VanillaItemTable_Init() { GET_ITEM(ITEM_BULLET_BAG_50, OBJECT_GI_DEKUPOUCH, GID_BULLET_BAG_50, 0x6C, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_LESSER, MOD_NONE, GI_BULLET_BAG_50), GET_ITEM_NONE, GET_ITEM_NONE, + GET_ITEM(ITEM_SHIP, OBJECT_GAMEPLAY_KEEP,GID_MAXIMUM,TEXT_CUSTOM_MESSAGE,0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_JUNK, MOD_NONE, GI_SHIP), GET_ITEM_NONE // GI_MAX - if you need to add to this table insert it before this entry. // clang-format on }; @@ -1306,6 +1308,9 @@ extern "C" void InitOTR(int argc, char* argv[]) { OTRExtScanner(); VanillaItemTable_Init(); DebugConsole_Init(); + CustomMessageManager::Instance->RegisterHooks(); + GameInteractor::Instance->RegisterOwnHooks(); + CustomItem::RegisterHooks(); InitMods(); ActorDB::AddBuiltInCustomActors(); diff --git a/soh/src/code/z_draw.c b/soh/src/code/z_draw.c index b1b4c1cc5bb..bd8a95c9f95 100644 --- a/soh/src/code/z_draw.c +++ b/soh/src/code/z_draw.c @@ -399,6 +399,10 @@ DrawItemTableEntry sDrawItemTable[] = { * Calls the corresponding draw function for the given draw ID */ void GetItem_Draw(PlayState* play, s16 drawId) { + // SoH [Enhancements] Prevent any UB here, GID_MAXIMUM useful for overriding GI draws + if (drawId < 0 || drawId >= GID_MAXIMUM) + return; + sDrawItemTable[drawId].drawFunc(play, drawId); } diff --git a/soh/src/code/z_en_item00.c b/soh/src/code/z_en_item00.c index 7fc89ee8349..9e9dea90a3e 100644 --- a/soh/src/code/z_en_item00.c +++ b/soh/src/code/z_en_item00.c @@ -478,8 +478,6 @@ void EnItem00_Init(Actor* thisx, PlayState* play) { Actor_SetScale(&this->actor, 0.03f); this->scale = 0.03f; break; - case ITEM00_SOH_GIVE_ITEM_ENTRY: - case ITEM00_SOH_GIVE_ITEM_ENTRY_GI: case ITEM00_SOH_DUMMY: this->unk_158 = 0; Actor_SetScale(&this->actor, 0.03f); @@ -772,9 +770,9 @@ void EnItem00_Update(Actor* thisx, PlayState* play) { (this->actor.params >= ITEM00_ARROWS_SMALL && this->actor.params <= ITEM00_SMALL_KEY) || this->actor.params == ITEM00_BOMBS_A || this->actor.params == ITEM00_ARROWS_SINGLE || this->actor.params == ITEM00_BOMBS_SPECIAL || - (this->actor.params >= ITEM00_BOMBCHU && this->actor.params <= ITEM00_SOH_GIVE_ITEM_ENTRY_GI)) { + (this->actor.params >= ITEM00_BOMBCHU && this->actor.params <= ITEM00_SOH_DUMMY)) { if (CVarGetInteger(CVAR_ENHANCEMENT("NewDrops"), 0) || - (this->actor.params >= ITEM00_SOH_DUMMY && this->actor.params <= ITEM00_SOH_GIVE_ITEM_ENTRY_GI)) { + (this->actor.params >= ITEM00_SOH_DUMMY && this->actor.params <= ITEM00_SOH_DUMMY)) { this->actor.shape.rot.y += 960; } else { this->actor.shape.rot.y = 0; diff --git a/soh/src/code/z_message_PAL.c b/soh/src/code/z_message_PAL.c index 25efbaadd1c..e698dd09aa4 100644 --- a/soh/src/code/z_message_PAL.c +++ b/soh/src/code/z_message_PAL.c @@ -2713,6 +2713,9 @@ void Message_OpenText(PlayState* play, u16 textId) { Font* font = &msgCtx->font; s16 textBoxType; + bool loadFromMessageTable = true; + GameInteractor_ExecuteOnOpenText(&textId, &loadFromMessageTable); + sDisplayNextMessageAsEnglish = false; if (msgCtx->msgMode == MSGMODE_NONE) { @@ -2782,7 +2785,9 @@ void Message_OpenText(PlayState* play, u16 textId) { gSaveContext.eventInf[0] = gSaveContext.eventInf[1] = gSaveContext.eventInf[2] = gSaveContext.eventInf[3] = 0; } - if (CustomMessage_RetrieveIfExists(play)) { + if (!loadFromMessageTable) { + // no-op + } else if (CustomMessage_RetrieveIfExists(play)) { osSyncPrintf("Found custom message"); if (gSaveContext.language == LANGUAGE_JPN) { sDisplayNextMessageAsEnglish = true; diff --git a/soh/src/code/z_parameter.c b/soh/src/code/z_parameter.c index 3b1d91f056a..d509c08eef2 100644 --- a/soh/src/code/z_parameter.c +++ b/soh/src/code/z_parameter.c @@ -1872,6 +1872,10 @@ u8 Return_Item(u8 itemID, ModIndex modId, ItemID returnItem) { * @return u8 */ u8 Item_Give(PlayState* play, u8 item) { + // SoH [Enhancements] Ignore ITEM_SHIP, used for CustomItem + if (item == ITEM_SHIP) + return ITEM_NONE; + // prevents getting sticks without the bag in case something got missed if (IS_RANDO && (item == ITEM_STICK || item == ITEM_STICKS_5 || item == ITEM_STICKS_10) && Randomizer_GetSettingValue(RSK_SHUFFLE_DEKU_STICK_BAG) && CUR_UPG_VALUE(UPG_STICKS) == 0) { @@ -2476,6 +2480,10 @@ u8 Item_CheckObtainability(u8 item) { s16 slot = SLOT(item); s32 temp; + // SoH [Enhancements] Ignore ITEM_SHIP, used for CustomItem + if (item == ITEM_SHIP) + return ITEM_NONE; + if (item >= ITEM_STICKS_5) { slot = SLOT(sExtraItemBases[item - ITEM_STICKS_5]); } diff --git a/soh/src/code/z_player_lib.c b/soh/src/code/z_player_lib.c index 49d9380bf66..a16fded0e0a 100644 --- a/soh/src/code/z_player_lib.c +++ b/soh/src/code/z_player_lib.c @@ -1576,49 +1576,24 @@ void func_800906D4(PlayState* play, Player* this, Vec3f* newTipPos) { } } -void Player_DrawGetItemIceTrap(PlayState* play, Player* this, Vec3f* refPos, s32 drawIdPlusOne, f32 height) { +void Player_DrawGetItemIceTrap(PlayState* play) { OPEN_DISPS(play->state.gfxCtx); - if (CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { - Gfx_SetupDL_25Opa(play->state.gfxCtx); + Matrix_Push(); - Matrix_Scale(0.2f, 0.2f, 0.2f, MTXMODE_APPLY); - gSPMatrix(POLY_OPA_DISP++, MATRIX_NEWMTX(play->state.gfxCtx), G_MTX_MODELVIEW | G_MTX_LOAD); + Gfx_SetupDL_25Xlu(play->state.gfxCtx); - gDPSetGrayscaleColor(POLY_OPA_DISP++, 75, 75, 75, 255); - gSPGrayscale(POLY_OPA_DISP++, true); + gSPSegment(POLY_XLU_DISP++, 0x08, + Gfx_TwoTexScrollEnvColor(play->state.gfxCtx, 0, 0, (0 - play->gameplayFrames) % 128, 32, 32, 1, 0, + (play->gameplayFrames * -2) % 128, 32, 32, 0, 50, 100, 255)); - gSPDisplayList(POLY_OPA_DISP++, (Gfx*)gSilverRockDL); + Matrix_Translate(0.0f, -40.0f, 0.0f, MTXMODE_APPLY); + Matrix_Scale(iceTrapScale, iceTrapScale, iceTrapScale, MTXMODE_APPLY); + gSPMatrix(POLY_XLU_DISP++, MATRIX_NEWMTX(play->state.gfxCtx), G_MTX_NOPUSH | G_MTX_LOAD | G_MTX_MODELVIEW); + gDPSetEnvColor(POLY_XLU_DISP++, 0, 50, 100, 255); + gSPDisplayList(POLY_XLU_DISP++, gEffIceFragment3DL); - gSPGrayscale(POLY_OPA_DISP++, false); - } else { - if (iceTrapScale < 0.01) { - iceTrapScale += 0.001f; - } else if (iceTrapScale < 0.8f) { - iceTrapScale += 0.2f; - } - gSPSegment(POLY_XLU_DISP++, 0x08, - Gfx_TwoTexScroll(play->state.gfxCtx, 0, 0, (0 - play->gameplayFrames) % 128, 32, 32, 1, 0, - (play->gameplayFrames * -2) % 128, 32, 32)); - - Matrix_Translate(0.0f, -40.0f, 0.0f, MTXMODE_APPLY); - Matrix_Scale(iceTrapScale, iceTrapScale, iceTrapScale, MTXMODE_APPLY); - gSPMatrix(POLY_XLU_DISP++, MATRIX_NEWMTX(play->state.gfxCtx), G_MTX_NOPUSH | G_MTX_LOAD | G_MTX_MODELVIEW); - gDPSetEnvColor(POLY_XLU_DISP++, 0, 50, 100, 255); - gSPDisplayList(POLY_XLU_DISP++, gEffIceFragment3DL); - - // Reset matrix for the fake item model because we're animating the size of the ice block around it before this. - Matrix_Translate(refPos->x + (3.3f * Math_SinS(this->actor.shape.rot.y)), refPos->y + height, - refPos->z + ((3.3f + (IREG(90) / 10.0f)) * Math_CosS(this->actor.shape.rot.y)), MTXMODE_NEW); - Matrix_RotateZYX(0, play->gameplayFrames * 1000, 0, MTXMODE_APPLY); - Matrix_Scale(0.2f, 0.2f, 0.2f, MTXMODE_APPLY); - // Draw fake item model. - if (this->getItemEntry.drawFunc != NULL) { - this->getItemEntry.drawFunc(play, &this->getItemEntry); - } else { - GetItem_Draw(play, drawIdPlusOne - 1); - } - } + Matrix_Pop(); CLOSE_DISPS(play->state.gfxCtx); } @@ -1638,11 +1613,7 @@ void Player_DrawGetItemImpl(PlayState* play, Player* this, Vec3f* refPos, s32 dr Matrix_RotateZYX(0, play->gameplayFrames * 1000, 0, MTXMODE_APPLY); Matrix_Scale(0.2f, 0.2f, 0.2f, MTXMODE_APPLY); - if (this->getItemEntry.modIndex == MOD_RANDOMIZER && this->getItemEntry.getItemId == RG_ICE_TRAP) { - Player_DrawGetItemIceTrap(play, this, refPos, drawIdPlusOne, height); - } else if (this->getItemEntry.modIndex == MOD_RANDOMIZER && this->getItemEntry.getItemId == RG_TRIFORCE_PIECE) { - Randomizer_DrawTriforcePieceGI(play, this->getItemEntry); - } else if (this->getItemEntry.drawFunc != NULL) { + if (this->getItemEntry.drawFunc != NULL) { this->getItemEntry.drawFunc(play, &this->getItemEntry); } else { GetItem_Draw(play, drawIdPlusOne - 1); diff --git a/soh/src/overlays/actors/ovl_player_actor/z_player.c b/soh/src/overlays/actors/ovl_player_actor/z_player.c index 2de7113d218..0edb897c41b 100644 --- a/soh/src/overlays/actors/ovl_player_actor/z_player.c +++ b/soh/src/overlays/actors/ovl_player_actor/z_player.c @@ -7339,9 +7339,7 @@ s32 Player_ActionHandler_2(Player* this, PlayState* play) { // Only skip cutscenes for drops when they're items/consumables from bushes/rocks/enemies. uint8_t isDropToSkip = (interactedActor->id == ACTOR_EN_ITEM00 && interactedActor->params != ITEM00_HEART_PIECE && - interactedActor->params != ITEM00_SMALL_KEY && - interactedActor->params != ITEM00_SOH_GIVE_ITEM_ENTRY && - interactedActor->params != ITEM00_SOH_GIVE_ITEM_ENTRY_GI) || + interactedActor->params != ITEM00_SMALL_KEY && interactedActor->params != ITEM00_NONE) || interactedActor->id == ACTOR_EN_KAREBABA || interactedActor->id == ACTOR_EN_DEKUBABA; // Skip cutscenes from picking up consumables with "Fast Pickup Text" enabled, even when the player