twitch chat as forced navi message

This commit is contained in:
dt 2025-08-04 16:51:14 -07:00
commit 3d2520cf80
3 changed files with 222 additions and 0 deletions

View file

@ -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
)

View file

@ -15,12 +15,86 @@
#include "soh/SaveManager.h"
#include "soh/ResourceManagerHelpers.h"
#include <stdio.h>
#include <stdlib.h>
#include <curl/curl.h>
// #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",

View file

@ -38,6 +38,74 @@
#include <stdlib.h>
#include <assert.h>
// region - New for checking twitch queue
#include <curl/curl.h>
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) {