summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--mm/2s2h/BenPort.cpp5
-rw-r--r--mm/2s2h/CustomItem/CustomItem.cpp214
-rw-r--r--mm/2s2h/CustomItem/CustomItem.h23
-rw-r--r--mm/2s2h/CustomMessage/CustomMessage.cpp146
-rw-r--r--mm/2s2h/CustomMessage/CustomMessage.h43
-rw-r--r--mm/2s2h/DeveloperTools/EventLog.cpp17
-rw-r--r--mm/2s2h/Enhancements/DemoBehavior.cpp150
-rw-r--r--mm/2s2h/Enhancements/Enhancements.cpp4
-rw-r--r--mm/2s2h/GameInteractor/GameInteractor.cpp119
-rw-r--r--mm/2s2h/GameInteractor/GameInteractor.h45
-rw-r--r--mm/include/z64item.h4
-rw-r--r--mm/src/code/z_message.c7
-rw-r--r--mm/src/code/z_parameter.c6
-rw-r--r--mm/src/overlays/actors/ovl_player_actor/z_player.c6
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>(&currentEvent)) {
+ // 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?