From 3d2520cf80cd2cb05d37a58f7af44b2eb81b37aa Mon Sep 17 00:00:00 2001 From: dt Date: Mon, 4 Aug 2025 16:51:14 -0700 Subject: [PATCH] twitch chat as forced navi message --- soh/CMakeLists.txt | 7 ++ soh/src/code/z_message_PAL.c | 97 ++++++++++++++ .../actors/ovl_player_actor/z_player.c | 118 ++++++++++++++++++ 3 files changed, 222 insertions(+) diff --git a/soh/CMakeLists.txt b/soh/CMakeLists.txt index e74c14f4c..f34a63ae7 100644 --- a/soh/CMakeLists.txt +++ b/soh/CMakeLists.txt @@ -282,6 +282,12 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Windows") endif() set_target_properties(${PROJECT_NAME} PROPERTIES MSVC_RUNTIME_LIBRARY ${MSVC_RUNTIME_LIBRARY_STR}) endif() + +################################################################################ +# Find/download curl Libs (For fetching twitch messages) +################################################################################ +find_package(CURL REQUIRED) + ################################################################################ # Find/download Dr Libs (For custom audio) ################################################################################ @@ -630,6 +636,7 @@ endif() ################################################################################ # Dependencies ################################################################################ +target_link_libraries(${PROJECT_NAME} PRIVATE CURL::libcurl) add_dependencies(${PROJECT_NAME} libultraship ) diff --git a/soh/src/code/z_message_PAL.c b/soh/src/code/z_message_PAL.c index fc63f96c8..26a7696a1 100644 --- a/soh/src/code/z_message_PAL.c +++ b/soh/src/code/z_message_PAL.c @@ -15,12 +15,86 @@ #include "soh/SaveManager.h" #include "soh/ResourceManagerHelpers.h" +#include +#include +#include + // #region SOH [NTSC] - Allows custom messages to work on japanese static bool sDisplayNextMessageAsEnglish = false; static u8 sLastLanguage = LANGUAGE_ENG; static u16 sTextBoxNum = 0; // #endregion +// #region text insert - from https://github.com/Daniel-Uzcategui/OOT/ +typedef struct { + char* originalMessage; + char* modifiedMessage; +} MessageData; + +struct MemoryStruct { + char *memory; + size_t size; +}; + +static size_t WriteMemoryCallback(void *contents, size_t size, size_t nmemb, void *userp) { + size_t realsize = size * nmemb; + struct MemoryStruct *mem = (struct MemoryStruct *)userp; + + char *ptr = realloc(mem->memory, mem->size + realsize + 1); + if(!ptr) { + /* out of memory! */ + printf("not enough memory (realloc returned NULL)\n"); + return 0; + } + + mem->memory = ptr; + memcpy(&(mem->memory[mem->size]), contents, realsize); + mem->size += realsize; + mem->memory[mem->size] = 0; + + return realsize; +} + +char *ModifyMessageThroughAPI(const char *originalMessage) { + CURL *curl; + CURLcode res; + struct MemoryStruct chunk; + + chunk.memory = malloc(1); /* will be grown as needed by the realloc above */ + chunk.size = 0; /* no data at this point */ + + curl_global_init(CURL_GLOBAL_DEFAULT); + curl = curl_easy_init(); + + if(curl) { + struct curl_slist *headers = NULL; + + headers = curl_slist_append(headers, "Content-Type: text/plain"); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + + curl_easy_setopt(curl, CURLOPT_URL, "http://localhost:5001/twitchMessage"); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, originalMessage); + + /* send all data to this function */ + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback); + + /* we pass our 'chunk' struct to the callback function */ + curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&chunk); + + res = curl_easy_perform(curl); + + if(res != CURLE_OK) + fprintf(stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res)); + + curl_easy_cleanup(curl); + } + + curl_global_cleanup(); + + return chunk.memory; +} +//endregion + s16 sTextFade = false; // original name: key_off_flag ? u8 D_8014B2F4 = 0; @@ -366,6 +440,29 @@ void Message_FindMessage(PlayState* play, u16 textId) { font->msgOffset = messageTableEntry->segment; font->msgLength = messageTableEntry->msgSize; + if (textId == 0x110 || textId == -0x110){ // our hijacked Navi textId + // region From Daniel-Uzcategui branch + // Dynamically allocate memory for originalMessage + char *originalMessage = (char *)malloc((font->msgLength + 1) * sizeof(char)); + if (originalMessage == NULL) { + // Handle error + fprintf(stderr, "Memory allocation failed!\n"); + return; + } + // Copy the found message to originalMessage + strncpy(originalMessage, foundSeg, font->msgLength); + originalMessage[font->msgLength] = '\0'; // Null-terminate the string + + // Send the original message to the API and get the modified message + char *modifiedMessage = ModifyMessageThroughAPI(originalMessage); + + // Use the modified message instead of the original message + font->msgOffset = modifiedMessage; + font->msgLength = strlen(modifiedMessage); + free(originalMessage); + //endregion + } + // "Message found!!!" osSyncPrintf(" メッセージが,見つかった!!! = %x " "(data=%x) (data0=%x) (data1=%x) (data2=%x) (data3=%x)\n", 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 3571ca135..10fde3ac8 100644 --- a/soh/src/overlays/actors/ovl_player_actor/z_player.c +++ b/soh/src/overlays/actors/ovl_player_actor/z_player.c @@ -38,6 +38,74 @@ #include #include +// region - New for checking twitch queue +#include + +struct QueueCheckResponse { + char* data; + size_t size; +}; + +static s32 sQueueCheckTimer = 0; +static bool sQueueWasEmpty = true; +static const s32 QUEUE_CHECK_INTERVAL = 120; // frames + +// write HTTP queue response +static size_t WriteQueueCheckCallback(void *contents, size_t size, size_t nmemb, void *userp) { + size_t realsize = size * nmemb; + struct QueueCheckResponse *response = (struct QueueCheckResponse *)userp; + + char *ptr = realloc(response->data, response->size + realsize + 1); + if (!ptr) { + return 0; + } + + response->data = ptr; + memcpy(&(response->data[response->size]), contents, realsize); + response->size += realsize; + response->data[response->size] = 0; + + return realsize; +} + +// check message queue is not empty +static bool CheckQueueNotEmpty() { + CURL *curl; + CURLcode res; + struct QueueCheckResponse response = {0}; + bool queueNotEmpty = false; + + curl = curl_easy_init(); + if (curl) { + response.data = malloc(1); + response.size = 0; + + curl_easy_setopt(curl, CURLOPT_URL, "http://localhost:5001/queueStatus"); + curl_easy_setopt(curl, CURLOPT_POST, 1L); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, ""); + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, 0L); // zero length, just need the response + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteQueueCheckCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&response); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 1L); // 1 second timeout + + res = curl_easy_perform(curl); + + if (res == CURLE_OK && response.data) { + if (strncmp(response.data, "true", 4) == 0) { + queueNotEmpty = true; + } + } + + curl_easy_cleanup(curl); + if (response.data) { + free(response.data); + } + } + + return queueNotEmpty; +} +// endregion + // Some player animations are played at this reduced speed, for reasons yet unclear. // This is called "adjusted" for now. #define PLAYER_ANIM_ADJUSTED_SPEED (2.0f / 3.0f) @@ -344,6 +412,53 @@ void Player_Action_80850C68(Player* this, PlayState* play); void Player_Action_80850E84(Player* this, PlayState* play); void Player_Action_CsAction(Player* this, PlayState* play); +void Player_CheckQueueAndSetNavi(Player* this, PlayState* play) { + // if we're in normal gameplay + if (play->csCtx.state != CS_STATE_IDLE || + this->csAction != 0 || + play->transitionTrigger != TRANS_TRIGGER_OFF || + gSaveContext.health == 0) { + return; + } + if (this->naviActor == NULL) { + return; // Navi actor doesn't exist + } + // Check if Navi is already busy talking + if (this->naviActor->flags & ACTOR_FLAG_TALK) { + return; + } + + // make sure the player is in a state where they can talk to Navi + if (this->stateFlags1 & (PLAYER_STATE1_IN_WATER | + PLAYER_STATE1_HANGING_OFF_LEDGE | + PLAYER_STATE1_INPUT_DISABLED | + PLAYER_STATE1_CLIMBING_LEDGE | + PLAYER_STATE1_GETTING_ITEM | + PLAYER_STATE1_TALKING | + PLAYER_STATE1_IN_CUTSCENE | + PLAYER_STATE1_CLIMBING_LADDER)) { + return; // player is in a state where they can't talk + } + + + sQueueCheckTimer--; + + if (sQueueCheckTimer <= 0) { + sQueueCheckTimer = QUEUE_CHECK_INTERVAL; + + bool queueNotEmpty = CheckQueueNotEmpty(); + + // queue has messages -> trigger Navi + if (queueNotEmpty) { + // check if Navi already has a textID ready to go + if (this->naviTextId == 0){ + this->naviTextId = -0x110; // appears to be an unused Navi textID we can hijack + // negative ID to force chatting with Navi + } + } + } +} + #pragma region[SoH] u8 gWalkSpeedToggle1; u8 gWalkSpeedToggle2; @@ -11934,6 +12049,9 @@ void Player_UpdateCommon(Player* this, PlayState* play, Input* input) { sControlInput = input; + // for twitch chat checking, regular checks during player main loop + Player_CheckQueueAndSetNavi(this, play); + if (this->unk_A86 < 0) { this->unk_A86++; if (this->unk_A86 == 0) {