diff options
| -rw-r--r-- | mm/2s2h/BenPort.cpp | 5 | ||||
| -rw-r--r-- | mm/2s2h/CustomItem/CustomItem.cpp | 214 | ||||
| -rw-r--r-- | mm/2s2h/CustomItem/CustomItem.h | 23 | ||||
| -rw-r--r-- | mm/2s2h/CustomMessage/CustomMessage.cpp | 146 | ||||
| -rw-r--r-- | mm/2s2h/CustomMessage/CustomMessage.h | 43 | ||||
| -rw-r--r-- | mm/2s2h/DeveloperTools/EventLog.cpp | 17 | ||||
| -rw-r--r-- | mm/2s2h/Enhancements/DemoBehavior.cpp | 150 | ||||
| -rw-r--r-- | mm/2s2h/Enhancements/Enhancements.cpp | 4 | ||||
| -rw-r--r-- | mm/2s2h/GameInteractor/GameInteractor.cpp | 119 | ||||
| -rw-r--r-- | mm/2s2h/GameInteractor/GameInteractor.h | 45 | ||||
| -rw-r--r-- | mm/include/z64item.h | 4 | ||||
| -rw-r--r-- | mm/src/code/z_message.c | 7 | ||||
| -rw-r--r-- | mm/src/code/z_parameter.c | 6 | ||||
| -rw-r--r-- | mm/src/overlays/actors/ovl_player_actor/z_player.c | 6 |
14 files changed, 767 insertions, 22 deletions
diff --git a/mm/2s2h/BenPort.cpp b/mm/2s2h/BenPort.cpp index 0c3c9dda8..01737a167 100644 --- a/mm/2s2h/BenPort.cpp +++ b/mm/2s2h/BenPort.cpp @@ -55,6 +55,8 @@ CrowdControl* CrowdControl::Instance; #include "2s2h/DeveloperTools/DebugConsole.h" #include "2s2h/DeveloperTools/DeveloperTools.h" #include "2s2h/SaveManager/SaveManager.h" +#include "2s2h/CustomMessage/CustomMessage.h" +#include "2s2h/CustomItem/CustomItem.h" // Resource Types/Factories #include "resource/type/Blob.h" @@ -517,6 +519,9 @@ extern "C" void InitOTR() { InitDeveloperTools(); GfxPatcher_ApplyNecessaryAuthenticPatches(); DebugConsole_Init(); + GameInteractor::Instance->RegisterOwnHooks(); + CustomItem::RegisterHooks(); + CustomMessage::RegisterHooks(); OTRMessage_Init(); OTRAudio_Init(); diff --git a/mm/2s2h/CustomItem/CustomItem.cpp b/mm/2s2h/CustomItem/CustomItem.cpp new file mode 100644 index 000000000..f6b84a495 --- /dev/null +++ b/mm/2s2h/CustomItem/CustomItem.cpp @@ -0,0 +1,214 @@ +#include "CustomItem.h" +#include <libultraship/libultraship.h> +#include "2s2h/CustomMessage/CustomMessage.h" +#include "2s2h/GameInteractor/GameInteractor.h" + +extern "C" { +#include "z64actor.h" +#include "functions.h" +#include "variables.h" +#include "macros.h" +} + +EnItem00* CustomItem::Spawn(f32 posX, f32 posY, f32 posZ, s16 rot, s16 flags, s16 params, ActorFunc actionFunc, + ActorFunc drawFunc) { + if (!gPlayState) { + return; + } + + Actor* actor = Actor_Spawn(&gPlayState->actorCtx, gPlayState, ACTOR_EN_ITEM00, posX, posY, posZ, flags, rot, params, + ITEM00_NOTHING); + EnItem00* enItem00 = (EnItem00*)actor; + + if (actionFunc != NULL) { + enItem00->actionFunc = (EnItem00ActionFunc)actionFunc; + } + + if (drawFunc != NULL) { + actor->draw = drawFunc; + } + + return enItem00; +} + +void CustomItem00_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); + } + + enItem00->unk152 = -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 CustomItem00_Draw(Actor* actor, PlayState* play) { + Matrix_Scale(30.0f, 30.0f, 30.0f, MTXMODE_APPLY); + GetItem_Draw(play, CUSTOM_ITEM_PARAM); +} + +void CustomItem00_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 = 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); + } + + 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::KILL_ON_TOUCH) { + // Pretty self explanatory, if the player is within range, kill the actor and call the action function + if ((actor->xzDistToPlayer <= 50.0f) && (fabsf(actor->playerHeightRel) <= fabsf(20.0f))) { + 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 (unk152 == -1) and the player is within range + if (enItem00->unk152 == -1 && (actor->xzDistToPlayer <= 50.0f) && + (fabsf(actor->playerHeightRel) <= fabsf(20.0f))) { + // Fire the action function + if (enItem00->actionFunc != NULL) { + enItem00->actionFunc(enItem00, play); + CUSTOM_ITEM_FLAGS |= CustomItem::CALLED_ACTION; + } + Audio_PlaySfx(NA_SE_SY_GET_ITEM); + // Set the unk152 to 15, this indicates the item has been picked up and will start the overhead animation + enItem00->unk152 = 15; + CUSTOM_ITEM_FLAGS |= CustomItem::STOP_BOBBING; + CUSTOM_ITEM_FLAGS |= CustomItem::KEEP_ON_PLAYER; + } + + // If the item has been picked up + if (enItem00->unk152 > 0) { + // Reduce the size a bit, but also makes it visible for HIDE_TILL_OVERHEAD + Actor_SetScale(actor, 0.010f); + + // Decrement the unk152, which will be used to bob the item up and down + enItem00->unk152--; + + // Account for the different heights of the player forms + f32 height = 45.0f; + switch (GET_PLAYER_FORM) { + case PLAYER_FORM_DEKU: + height = 35.0f; + break; + case PLAYER_FORM_FIERCE_DEITY: + height = 90.0f; + break; + case PLAYER_FORM_GORON: + height = 75.0f; + break; + case PLAYER_FORM_ZORA: + height = 60.0f; + break; + } + + // Bob the item up and down + actor->world.pos.y += (height + (Math_SinS(enItem00->unk152 * 15000) * (enItem00->unk152 * 0.3f))); + } + + // Finally, once the bobbing animation is done, kill the actor + if (enItem00->unk152 == 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->unk152 == -1) { + Actor_OfferGetItem(actor, play, GI_SHIP, 50.0f, 20.0f); + } else { + if (enItem00->unk152 == -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; + } + + // Begin incrementing the unk152, indicating the item has been picked up + enItem00->unk152++; + + // For the first 20 frames, wait while the player's animation plays + if (enItem00->unk152 >= 20) { + // After the first 20 frames, show the item and call the action function + if (enItem00->unk152 == 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); + + f32 height = 45.0f; + switch (GET_PLAYER_FORM) { + case PLAYER_FORM_DEKU: + height = 35.0f; + break; + case PLAYER_FORM_FIERCE_DEITY: + height = 90.0f; + break; + case PLAYER_FORM_GORON: + height = 75.0f; + break; + case PLAYER_FORM_ZORA: + height = 60.0f; + break; + } + + actor->world.pos.y += height; + } + + // Once the player is no longer in the "Give Item" state, kill the actor + if (!(player->stateFlags1 & PLAYER_STATE1_400)) { + Actor_Kill(actor); + } + } + } +} + +void CustomItem::RegisterHooks() { + GameInteractor::Instance->RegisterGameHookForID<GameInteractor::ShouldActorInit>( + ACTOR_EN_ITEM00, [](Actor* actor, bool* should) { + if (actor->params != ITEM00_NOTHING) { + return; + } + + actor->init = CustomItem00_Init; + actor->update = CustomItem00_Update; + actor->draw = CustomItem00_Draw; + actor->destroy = NULL; + + // 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; + }); +} diff --git a/mm/2s2h/CustomItem/CustomItem.h b/mm/2s2h/CustomItem/CustomItem.h new file mode 100644 index 000000000..aa97de02e --- /dev/null +++ b/mm/2s2h/CustomItem/CustomItem.h @@ -0,0 +1,23 @@ +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 +}; +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/mm/2s2h/CustomMessage/CustomMessage.cpp b/mm/2s2h/CustomMessage/CustomMessage.cpp new file mode 100644 index 000000000..21eef0623 --- /dev/null +++ b/mm/2s2h/CustomMessage/CustomMessage.cpp @@ -0,0 +1,146 @@ + +#include "CustomMessage.h" +#include <libultraship/bridge.h> +#include "2s2h/GameInteractor/GameInteractor.h" +#include <map> + +extern "C" { +#include "variables.h" +extern f32 sNESFontWidths[160]; +} + +CustomMessage::Entry activeCustomMessage; + +void CustomMessage::StartTextbox(std::string msg, CustomMessage::Entry options) { + options.msg = msg; + activeCustomMessage = options; + + Message_StartTextbox(gPlayState, CUSTOM_MESSAGE_ID, &GET_PLAYER(gPlayState)->actor); +} + +void CustomMessage::SetActiveCustomMessage(std::string msg, CustomMessage::Entry options) { + options.msg = msg; + activeCustomMessage = options; +} + +void CustomMessage::Replace(std::string* msg, const std::string& placeholder, const std::string& value) { + size_t pos = 0; + while ((pos = msg->find(placeholder, pos)) != std::string::npos) { + msg->replace(pos, placeholder.length(), value); + pos += value.length(); + } +} + +void CustomMessage::AddLineBreaks(std::string* msg) { + const float MAX_TEXTBOX_WIDTH = 300.0f; + const int MAX_LINES_PER_PAGE = 4; + + float currentLineWidth = 0.0f; + int currentLineCount = 0; + size_t lastSpaceIndex = std::string::npos; + + for (size_t i = 0; i < msg->size(); ++i) { + char currentChar = (*msg)[i]; + + if (currentChar >= 0x20 && currentChar < 0x20 + sizeof(sNESFontWidths) / sizeof(sNESFontWidths[0])) { + currentLineWidth += sNESFontWidths[currentChar - 0x20]; + } + + if (currentChar == ' ') { + lastSpaceIndex = i; + } + if (currentLineWidth > MAX_TEXTBOX_WIDTH) { + if (lastSpaceIndex != std::string::npos) { + (*msg)[lastSpaceIndex] = 0x11; + i = lastSpaceIndex; + } else { + msg->insert(i, 1, 0x11); + } + + currentLineWidth = 0.0f; + lastSpaceIndex = std::string::npos; + ++currentLineCount; + + if (currentLineCount >= MAX_LINES_PER_PAGE) { + msg->insert(i + 1, 1, 0x10); + ++i; + currentLineCount = 0; + } + } + } +} + +// Ensure that the message ends with the message end character +void CustomMessage::EnsureMessageEnd(std::string* msg) { + if (msg->back() != 0xBF) { + msg->push_back(0xBF); + } +} + +CustomMessage::Entry CustomMessage::LoadVanillaMessageTableEntry(u16 textId) { + MessageContext* msgCtx = &gPlayState->msgCtx; + MessageTableEntry* msgEntry = msgCtx->messageEntryTableNes; + while (msgEntry->textId != 0xFFFF) { + if (msgEntry->textId == textId) { + break; + } + msgEntry++; + } + + CustomMessage::Entry entry; + + entry.textboxType = msgEntry->segment[0]; + entry.textboxYPos = msgEntry->segment[1]; + entry.icon = msgEntry->segment[2]; + entry.nextMessageID = (msgEntry->segment[3] << 8) | msgEntry->segment[4]; + entry.firstItemCost = (msgEntry->segment[5] << 8) | msgEntry->segment[6]; + entry.secondItemCost = (msgEntry->segment[7] << 8) | msgEntry->segment[8]; + entry.msg = std::string(msgEntry->segment + MESSAGE_HEADER_SIZE, msgEntry->msgSize - MESSAGE_HEADER_SIZE); + + return entry; +} + +void CustomMessage::LoadCustomMessageIntoFont(CustomMessage::Entry entry) { + MessageContext* msgCtx = &gPlayState->msgCtx; + Font* font = &msgCtx->font; + + char buff[1280] = { 0 }; + + // Copy message header + buff[0] = entry.textboxType; + buff[1] = entry.textboxYPos; + buff[2] = entry.icon; + buff[3] = (entry.nextMessageID & 0xFF00) >> 8; + buff[4] = (entry.nextMessageID & 0x00FF); + buff[5] = (entry.firstItemCost & 0xFF00) >> 8; + buff[6] = (entry.firstItemCost & 0x00FF); + buff[7] = (entry.secondItemCost & 0xFF00) >> 8; + buff[8] = (entry.secondItemCost & 0x00FF); + buff[9] = 0xFF; + buff[10] = 0xFF; + + if (entry.autoFormat) { + CustomMessage::AddLineBreaks(&entry.msg); + CustomMessage::EnsureMessageEnd(&entry.msg); + CustomMessage::Replace(&entry.msg, "\n", "\x11"); + } + + // If message is too long, truncate it and add the message end character + if (entry.msg.length() > BUFFER_SIZE - MESSAGE_HEADER_SIZE) { + entry.msg = entry.msg.substr(0, BUFFER_SIZE - MESSAGE_HEADER_SIZE - 1); + entry.msg += '\xBF'; + } + + memcpy(buff + MESSAGE_HEADER_SIZE, entry.msg.c_str(), entry.msg.length()); + + msgCtx->msgLength = entry.msg.length() + MESSAGE_HEADER_SIZE; + memcpy(&font->msgBuf, buff, msgCtx->msgLength); +} + +void CustomMessage::RegisterHooks() { + GameInteractor::Instance->RegisterGameHookForID<GameInteractor::OnOpenText>( + CUSTOM_MESSAGE_ID, [](u16* textId, bool* loadFromMessageTable) { + *loadFromMessageTable = false; + CustomMessage::LoadCustomMessageIntoFont(activeCustomMessage); + }); +} diff --git a/mm/2s2h/CustomMessage/CustomMessage.h b/mm/2s2h/CustomMessage/CustomMessage.h new file mode 100644 index 000000000..0cb509706 --- /dev/null +++ b/mm/2s2h/CustomMessage/CustomMessage.h @@ -0,0 +1,43 @@ +#ifndef CUSTOM_MESSAGE_H +#define CUSTOM_MESSAGE_H + +// Not really sure what the best ID is for this, but it needs to be between 0-255 +// because it's used as a u8 somewhere in the chain +#define CUSTOM_MESSAGE_ID 0x004B +#define BUFFER_SIZE 1280 +#define MESSAGE_HEADER_SIZE 11 + +#ifdef __cplusplus + +extern "C" { +#include "variables.h" +} + +#include <string> + +namespace CustomMessage { +struct Entry { + uint8_t textboxType = 0; + uint8_t textboxYPos = 0; + uint8_t icon = 0xFE; // No Icon + uint16_t nextMessageID = 0xFFFF; + uint16_t firstItemCost = 0xFFFF; + uint16_t secondItemCost = 0xFFFF; + bool autoFormat = true; + std::string msg = ""; +}; + +void RegisterHooks(); +void StartTextbox(std::string msg, Entry options = {}); +void SetActiveCustomMessage(std::string msg, Entry options = {}); + +// Helpers +void Replace(std::string* msg, const std::string& placeholder, const std::string& value); +void AddLineBreaks(std::string* msg); +void EnsureMessageEnd(std::string* msg); +Entry LoadVanillaMessageTableEntry(u16 textId); +void LoadCustomMessageIntoFont(Entry entry); +} // namespace CustomMessage + +#endif // __cplusplus +#endif // CUSTOM_MESSAGE_H diff --git a/mm/2s2h/DeveloperTools/EventLog.cpp b/mm/2s2h/DeveloperTools/EventLog.cpp index 6eacb3b03..33a7b6256 100644 --- a/mm/2s2h/DeveloperTools/EventLog.cpp +++ b/mm/2s2h/DeveloperTools/EventLog.cpp @@ -239,14 +239,15 @@ void RegisterEventLogHooks() { TrimEventLog(); }); - onOpenTextHookId = GameInteractor::Instance->RegisterGameHook<GameInteractor::OnOpenText>([](s16 textId) { - eventLogEntries.insert(eventLogEntries.begin(), { - .timestamp = CurrentTime(), - .type = EVENT_LOG_ENTRY_TYPE_OPEN_TEXT, - .meta = fmt::format("0x{:02x}", textId), - }); - TrimEventLog(); - }); + onOpenTextHookId = GameInteractor::Instance->RegisterGameHook<GameInteractor::OnOpenText>( + [](u16* textId, bool* loadFromMessageTable) { + eventLogEntries.insert(eventLogEntries.begin(), { + .timestamp = CurrentTime(), + .type = EVENT_LOG_ENTRY_TYPE_OPEN_TEXT, + .meta = fmt::format("0x{:02x}", *textId), + }); + TrimEventLog(); + }); onItemGiveHookId = GameInteractor::Instance->RegisterGameHook<GameInteractor::OnItemGive>([](u8 item) { eventLogEntries.insert(eventLogEntries.begin(), { diff --git a/mm/2s2h/Enhancements/DemoBehavior.cpp b/mm/2s2h/Enhancements/DemoBehavior.cpp new file mode 100644 index 000000000..0f5efbd9c --- /dev/null +++ b/mm/2s2h/Enhancements/DemoBehavior.cpp @@ -0,0 +1,150 @@ +#include "libultraship/libultraship.h" +#include "2s2h/GameInteractor/GameInteractor.h" +#include "2s2h/CustomMessage/CustomMessage.h" +#include "2s2h/CustomItem/CustomItem.h" + +extern "C" { +#include "z64actor.h" +#include "functions.h" +#include "z64item.h" + +#include "overlays/actors/ovl_Obj_Tsubo/z_obj_tsubo.h" +} + +// This file houses various examples of using the systems we have built to modify the game in various ways +void RegisterDemoBehavior() { + // Demonstrates some capabilities of CustomItem + GameInteractor::Instance->RegisterGameHookForID<GameInteractor::ShouldActorInit>( + ACTOR_EN_ITEM00, [](Actor* actor, bool* should) { + // South Clock Town PoH + if (actor->params != 2566) { + return; + } + *should = false; + + // Spawn a dummy custom item, will just be killed when you touch it and run it's action func + CustomItem::Spawn( + actor->world.pos.x, actor->world.pos.y, actor->world.pos.z, actor->shape.rot.y, + CustomItem::KILL_ON_TOUCH, GID_RUPEE_GREEN, + [](Actor* actor, PlayState* play) { + Player* player = GET_PLAYER(play); + + // You can put whatever you want here. For instance you can use the GI queue to queue up a different + // kind of item give: + GameInteractor::Instance->events.push_back(GIEventGiveItem{ + .showGetItemCutscene = true, + .param = GID_RUPEE_GREEN, + .giveItem = + [](Actor* actor, PlayState* play) { + if (CUSTOM_ITEM_FLAGS & CustomItem::GIVE_ITEM_CUTSCENE) { + CustomMessage::SetActiveCustomMessage("You found Greg!"); + } else { + CustomMessage::StartTextbox( + "You found Greg! (While you were jumping or something) \x1C\x02\x10"); + } + + Rupees_ChangeBy(1); + }, + .drawItem = + [](Actor* actor, PlayState* play) { + Matrix_Scale(30.0f, 30.0f, 30.0f, MTXMODE_APPLY); + GetItem_Draw(play, CUSTOM_ITEM_PARAM); + }, + }); + + // Or you can spawn another CustomItem to give an item that way, but the GI queue is more flexible + // and reliable + CustomItem::Spawn( + player->actor.world.pos.x, player->actor.world.pos.y, player->actor.world.pos.z, 0, + CustomItem::GIVE_ITEM_CUTSCENE | CustomItem::HIDE_TILL_OVERHEAD | CustomItem::KEEP_ON_PLAYER, 0, + [](Actor* actor, PlayState* play) { + CustomMessage::SetActiveCustomMessage("Loser! You thought it was Greg but you get nothing.", + { + .textboxType = 2, + }); + }, + [](Actor* actor, PlayState* play) { + // Draw Nothing + }); + }, + [](Actor* actor, PlayState* play) { + Matrix_Scale(30.0f, 30.0f, 30.0f, MTXMODE_APPLY); + GetItem_Draw(play, actor->home.rot.z); + }); + }); + + // Example doing a give item while a player is in the middle of an action (backflip) + GameInteractor::Instance->RegisterGameHookForID<GameInteractor::OnActorUpdate>(ACTOR_PLAYER, [](Actor* actor) { + Player* player = (Player*)actor; + static int backflipTimer = 0; + + if (player->stateFlags2 & PLAYER_STATE2_80000) { + backflipTimer++; + + if (backflipTimer == 4) { + player->actor.freezeTimer = 30; + CustomItem::Spawn(player->actor.world.pos.x, player->actor.world.pos.y, player->actor.world.pos.z, 0, + CustomItem::GIVE_OVERHEAD | CustomItem::KEEP_ON_PLAYER, GID_RUPEE_GREEN, + [](Actor* actor, PlayState* play) { + Rupees_ChangeBy(1); + CustomMessage::StartTextbox("Nice flip!\x1C\x02\x10", { + .textboxType = 2, + }); + }); + } + } else { + backflipTimer = 0; + } + }); + + GameInteractor::Instance->RegisterGameHookForID<GameInteractor::OnActorKill>(ACTOR_OBJ_TSUBO, [](Actor* actor) { + if (actor->room == gPlayState->roomCtx.curRoom.num) { + // Open a custom message + CustomMessage::StartTextbox("STOP BREAKING MY POTS!", { + .textboxType = 2, + .icon = 0x3D, + }); + + // Pot Hydra + // GameInteractor::Instance->events.push_back(GIEventSpawnActor{ + // .actorId = ACTOR_OBJ_TSUBO, + // .posX = 50.0f, + // .posY = 0, + // .posZ = -40.0f, // Behind the player + // .relativeCoords = true, + // }); + // GameInteractor::Instance->events.push_back(GIEventSpawnActor{ + // .actorId = ACTOR_OBJ_TSUBO, + // .posX = -50.0f, + // .posY = 0, + // .posZ = -50.0f, // Behind the player + // .relativeCoords = true, + // }); + } + }); + + // Full message replacement (Sign in South Clock Town) + GameInteractor::Instance->RegisterGameHookForID<GameInteractor::OnOpenText>( + 0x1C18, [](u16* textId, bool* loadFromMessageTable) { + CustomMessage::Entry entry = { + .icon = 0x10, + .msg = "Hello World\n" + "You are in Clock Town\n" + " - Moon", + }; + + CustomMessage::LoadCustomMessageIntoFont(entry); + *loadFromMessageTable = false; + }); + + // Partial message replacement (Title Card for South Clock Town) + GameInteractor::Instance->RegisterGameHookForID<GameInteractor::OnOpenText>( + 0x104, [](u16* textId, bool* loadFromMessageTable) { + auto entry = CustomMessage::LoadVanillaMessageTableEntry(*textId); + + CustomMessage::Replace(&entry.msg, "Clock Town", "Hyrule"); + + CustomMessage::LoadCustomMessageIntoFont(entry); + *loadFromMessageTable = false; + }); +} diff --git a/mm/2s2h/Enhancements/Enhancements.cpp b/mm/2s2h/Enhancements/Enhancements.cpp index 0e014d318..d986dccfd 100644 --- a/mm/2s2h/Enhancements/Enhancements.cpp +++ b/mm/2s2h/Enhancements/Enhancements.cpp @@ -70,4 +70,8 @@ void InitEnhancements() { // Modes RegisterPlayAsKafei(); RegisterTimeMovesWhenYouMove(); + + // Uncomment to enable the demo behavior, this shows of different modding capabilities + // void RegisterDemoBehavior(); + // RegisterDemoBehavior(); } diff --git a/mm/2s2h/GameInteractor/GameInteractor.cpp b/mm/2s2h/GameInteractor/GameInteractor.cpp index fbaf19930..a9fb3afe8 100644 --- a/mm/2s2h/GameInteractor/GameInteractor.cpp +++ b/mm/2s2h/GameInteractor/GameInteractor.cpp @@ -1,12 +1,15 @@ #include "GameInteractor.h" #include "spdlog/spdlog.h" +#include <libultraship/bridge.h> +#include "2s2h/CustomItem/CustomItem.h" +#include "2s2h/CustomMessage/CustomMessage.h" extern "C" { #include "z64actor.h" +#include "variables.h" +#include "functions.h" } -#include <libultraship/bridge.h> - void GameInteractor_ExecuteOnGameStateMainFinish() { GameInteractor::Instance->ExecuteHooks<GameInteractor::OnGameStateMainFinish>(); } @@ -208,11 +211,11 @@ void GameInteractor_ExecuteOnPassPlayerInputs(Input* input) { GameInteractor::Instance->ExecuteHooksForFilter<GameInteractor::OnPassPlayerInputs>(input); } -void GameInteractor_ExecuteOnOpenText(u16 textId) { - SPDLOG_DEBUG("OnOpenText: textId: {}", textId); - GameInteractor::Instance->ExecuteHooks<GameInteractor::OnOpenText>(textId); - GameInteractor::Instance->ExecuteHooksForID<GameInteractor::OnOpenText>(textId, textId); - GameInteractor::Instance->ExecuteHooksForFilter<GameInteractor::OnOpenText>(textId); +void GameInteractor_ExecuteOnOpenText(u16* textId, bool* loadFromMessageTable) { + SPDLOG_DEBUG("OnOpenText: textId: {}", *textId); + GameInteractor::Instance->ExecuteHooks<GameInteractor::OnOpenText>(textId, loadFromMessageTable); + GameInteractor::Instance->ExecuteHooksForID<GameInteractor::OnOpenText>(*textId, textId, loadFromMessageTable); + GameInteractor::Instance->ExecuteHooksForFilter<GameInteractor::OnOpenText>(textId, loadFromMessageTable); } bool GameInteractor_ShouldItemGive(u8 item) { @@ -314,3 +317,105 @@ uint32_t GameInteractor_Dpad(GIDpadType type, uint32_t buttonCombo) { return result; } + +void ProcessEvents(Actor* actor) { + 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_80) { + return; + } + + // If there is an event active, stop + const auto& currentEvent = GameInteractor::Instance->currentEvent; + if (auto e = std::get_if<GIEventNone>(¤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<GIEventGiveItem>(&nextEvent)) { + EnItem00* enItem00; + // 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_1000 | PLAYER_STATE1_2000 | PLAYER_STATE1_4000 | PLAYER_STATE1_40000 | + PLAYER_STATE1_80000 | PLAYER_STATE1_100000 | PLAYER_STATE1_200000)) || + (Player_GetExplosiveHeld(player) > PLAYER_EXPLOSIVE_NONE)) { + enItem00 = CustomItem::Spawn( + player->actor.world.pos.x, player->actor.world.pos.y, player->actor.world.pos.z, 0, + CustomItem::GIVE_OVERHEAD | CustomItem::HIDE_TILL_OVERHEAD | CustomItem::KEEP_ON_PLAYER, e->param, + [](Actor* actor, PlayState* play) { + Player* player = GET_PLAYER(gPlayState); + const auto& nextEvent = GameInteractor::Instance->currentEvent; + if (auto e = std::get_if<GIEventGiveItem>(&nextEvent)) { + e->giveItem(actor, play); + if (e->showGetItemCutscene) { + player->actor.freezeTimer = 30; + } + } + }, + e->drawItem); + } else { + enItem00 = CustomItem::Spawn( + player->actor.world.pos.x, player->actor.world.pos.y, player->actor.world.pos.z, 0, + CustomItem::GIVE_ITEM_CUTSCENE | CustomItem::HIDE_TILL_OVERHEAD | CustomItem::KEEP_ON_PLAYER, e->param, + e->giveItem, 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->currentEvent = GIEventNone{}; + }; + } else if (auto e = std::get_if<GIEventTransition>(&nextEvent)) { + gPlayState->nextEntrance = e->entrance; + gSaveContext.nextCutsceneIndex = e->cutsceneIndex; + gPlayState->transitionTrigger = e->transitionTrigger; + gPlayState->transitionType = e->transitionType; + GameInteractor::Instance->currentEvent = GIEventNone{}; + } else if (auto e = std::get_if<GIEventSpawnActor>(&nextEvent)) { + // if true, the coordinates are made relative to the player's position and rotation, 0 rotation is facing the + // same direction as the player, x+ is to the players right, y+ is up, z+ is in front of the player + if (e->relativeCoords) { + f32 x = player->actor.world.pos.x; + f32 y = player->actor.world.pos.y; + f32 z = player->actor.world.pos.z; + f32 s = sin(player->actor.world.rot.y); + f32 c = cos(player->actor.world.rot.y); + f32 x2 = e->posX * c - e->posZ * s; + f32 z2 = e->posX * s + e->posZ * c; + Actor_Spawn(&gPlayState->actorCtx, gPlayState, e->actorId, x + x2, y + e->posY, z + z2, 0, + e->rot + player->actor.world.rot.y, 0, e->params); + } else { + Actor_Spawn(&gPlayState->actorCtx, gPlayState, e->actorId, e->posX, e->posY, e->posZ, 0, e->rot, 0, + e->params); + } + GameInteractor::Instance->currentEvent = GIEventNone{}; + } + + GameInteractor::Instance->events.erase(GameInteractor::Instance->events.begin()); +} + +void GameInteractor::RegisterOwnHooks() { + GameInteractor::Instance->RegisterGameHookForID<GameInteractor::OnActorUpdate>(ACTOR_PLAYER, ProcessEvents); +} diff --git a/mm/2s2h/GameInteractor/GameInteractor.h b/mm/2s2h/GameInteractor/GameInteractor.h index d73e763ae..377989f1b 100644 --- a/mm/2s2h/GameInteractor/GameInteractor.h +++ b/mm/2s2h/GameInteractor/GameInteractor.h @@ -84,6 +84,7 @@ typedef enum { #ifdef __cplusplus +#include "2s2h/CustomMessage/CustomMessage.h" #include <vector> #include <functional> #include <unordered_map> @@ -98,12 +99,50 @@ typedef uint32_t HOOK_ID; typedef std::function<bool args> filter; \ } +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; +}; + +struct GIEventSpawnActor { + s16 actorId; + f32 posX; + f32 posY; + f32 posZ; + s16 rot; + s32 params; + // if true, the coordinates are made relative to the player's position and rotation, 0 rotation is facing the same + // direction as the player, x+ is to the players right, y+ is up, z+ is in front of the player + bool relativeCoords; +}; + +struct GIEventTransition { + u16 entrance; + u16 cutsceneIndex; + s8 transitionTrigger; + u8 transitionType; +}; + +typedef std::variant<GIEventNone, GIEventGiveItem, GIEventSpawnActor, GIEventTransition> GIEvent; + class GameInteractor { public: static GameInteractor* Instance; + void RegisterOwnHooks(); + // Game State - class State {}; + std::vector<GIEvent> events = {}; + GIEvent currentEvent = GIEventNone(); // Game Hooks HOOK_ID nextHookId = 1; @@ -308,7 +347,7 @@ class GameInteractor { DEFINE_HOOK(OnPassPlayerInputs, (Input * input)); - DEFINE_HOOK(OnOpenText, (u16 textId)); + DEFINE_HOOK(OnOpenText, (u16 * textId, bool* loadFromMessageTable)); DEFINE_HOOK(ShouldItemGive, (u8 item, bool* should)); DEFINE_HOOK(OnItemGive, (u8 item)); @@ -358,7 +397,7 @@ void GameInteractor_ExecuteOnCameraChangeSettingsFlags(Camera* camera); void GameInteractor_ExecuteOnPassPlayerInputs(Input* input); -void GameInteractor_ExecuteOnOpenText(u16 textId); +void GameInteractor_ExecuteOnOpenText(u16* textId, bool* loadFromMessageTable); bool GameInteractor_ShouldItemGive(u8 item); void GameInteractor_ExecuteOnItemGive(u8 item); diff --git a/mm/include/z64item.h b/mm/include/z64item.h index 1378d7562..688edfd50 100644 --- a/mm/include/z64item.h +++ b/mm/include/z64item.h @@ -326,6 +326,7 @@ typedef enum ItemId { /* 0xB1 */ ITEM_MAP_POINT_MOUNTAIN_VILLAGE, /* 0xB2 */ ITEM_MAP_POINT_MILK_ROAD, /* 0xB3 */ ITEM_MAP_POINT_ZORA_CAPE, + /* 0xB4 */ ITEM_SHIP, // 2S2H [Enhancement] Added to enable custom item gives /* 0xB8 */ ITEM_B8 = 0xB8, /* 0xB9 */ ITEM_B9, /* 0xBA */ ITEM_BA, @@ -546,7 +547,8 @@ typedef enum GetItemId { /* 0xB7 */ GI_TINGLE_MAP_ROMANI_RANCH, /* 0xB8 */ GI_TINGLE_MAP_GREAT_BAY, /* 0xB9 */ GI_TINGLE_MAP_STONE_TOWER, - /* 0xBA */ GI_MAX + /* 0xBA */ GI_SHIP, // 2S2H [Enhancement] Added to enable custom item gives + /* 0xBB */ GI_MAX } GetItemId; typedef enum GetItemDrawId { diff --git a/mm/src/code/z_message.c b/mm/src/code/z_message.c index 7f0c82be5..493ae2a9f 100644 --- a/mm/src/code/z_message.c +++ b/mm/src/code/z_message.c @@ -3280,7 +3280,8 @@ void Message_OpenText(PlayState* play, u16 textId) { Player* player = GET_PLAYER(play); f32 var_fv0; - GameInteractor_ExecuteOnOpenText(textId); + bool loadFromMessageTable = true; + GameInteractor_ExecuteOnOpenText(&textId, &loadFromMessageTable); // BENTODO do this somewhere else gSaveContext.options.language = LANGUAGE_ENG; @@ -3358,7 +3359,9 @@ void Message_OpenText(PlayState* play, u16 textId) { sCharTexScale = 1024.0f / msgCtx->textCharScale; D_801F6B08 = 1024.0f / var_fv0; // BENTODO all of these - if (msgCtx->textIsCredits) { + if (!loadFromMessageTable) { + // no-op + } else if (msgCtx->textIsCredits) { Message_FindCreditsMessage(play, textId); MessageTableEntry* msgEntry = (MessageTableEntry*)font->messageStart; msgCtx->msgLength = msgEntry->msgSize; diff --git a/mm/src/code/z_parameter.c b/mm/src/code/z_parameter.c index 66bb29055..13c5e2901 100644 --- a/mm/src/code/z_parameter.c +++ b/mm/src/code/z_parameter.c @@ -4005,7 +4005,7 @@ u8 Item_GiveImpl(PlayState* play, u8 item) { // #region 2S2H [Enhancements] This is our wrapper around the original Item_Give function for hooking purposes u8 Item_Give(PlayState* play, u8 item) { - if (!GameInteractor_ShouldItemGive(item)) { + if (!GameInteractor_ShouldItemGive(item) || item == ITEM_SHIP) { return ITEM_NONE; } @@ -4021,6 +4021,10 @@ u8 Item_CheckObtainabilityImpl(u8 item) { u8 slot; u8 bottleSlot; + if (item == ITEM_SHIP) { + return ITEM_NONE; + } + slot = SLOT(item); if (item >= ITEM_DEKU_STICKS_5) { slot = SLOT(sExtraItemBases[item - ITEM_DEKU_STICKS_5]); diff --git a/mm/src/overlays/actors/ovl_player_actor/z_player.c b/mm/src/overlays/actors/ovl_player_actor/z_player.c index 397dfdd0c..069801e4c 100644 --- a/mm/src/overlays/actors/ovl_player_actor/z_player.c +++ b/mm/src/overlays/actors/ovl_player_actor/z_player.c @@ -46,6 +46,7 @@ #include "objects/object_link_child/object_link_child.h" #include "2s2h/GameInteractor/GameInteractor.h" +#include "2s2h/CustomMessage/CustomMessage.h" #define THIS ((Player*)thisx) @@ -2565,6 +2566,11 @@ GetItemEntry sGetItemTable[GI_MAX - 1] = { // GI_TINGLE_MAP_STONE_TOWER GET_ITEM(ITEM_TINGLE_MAP, OBJECT_GI_FIELDMAP, GID_TINGLE_MAP, 0xB9, GIFIELD(GIFIELD_20 | GIFIELD_NO_COLLECTIBLE, 0), CHEST_ANIM_LONG), + // #region 2S2H [Enhancement] Added to enable custom item gives + // GI_SHIP + GET_ITEM(ITEM_SHIP, OBJECT_UNSET_0, GID_NONE, CUSTOM_MESSAGE_ID, GIFIELD(GIFIELD_20 | GIFIELD_NO_COLLECTIBLE, 0), + 0), + // #endregion }; // Player_UpdateCurrentGetItemDrawId? |
