#include "soh/Enhancements/game-interactor/GameInteractor.h" #include "soh/Enhancements/speechsynthesizer/SpeechSynthesizer.h" #include #include #include #include #include #include "soh/OTRGlobals.h" #include "message_data_static.h" #include "overlays/gamestates/ovl_file_choose/file_choose.h" #include "soh/Enhancements/boss-rush/BossRush.h" extern "C" { extern MapData* gMapData; extern SaveContext gSaveContext; extern PlayState* gPlayState; } typedef enum { /* 0x00 */ TEXT_BANK_SCENES, /* 0x01 */ TEXT_BANK_MISC, /* 0x02 */ TEXT_BANK_KALEIDO, /* 0x03 */ TEXT_BANK_FILECHOOSE, } TextBank; nlohmann::json sceneMap = nullptr; nlohmann::json miscMap = nullptr; nlohmann::json kaleidoMap = nullptr; nlohmann::json fileChooseMap = nullptr; // MARK: - Helpers std::string GetParameritizedText(std::string key, TextBank bank, const char* arg) { switch (bank) { case TEXT_BANK_SCENES: { return sceneMap[key].get(); break; } case TEXT_BANK_MISC: { auto value = miscMap[key].get(); std::string searchString = "$0"; size_t index = value.find(searchString); if (index != std::string::npos) { assert(arg != nullptr); value.replace(index, searchString.size(), std::string(arg)); return value; } else { return value; } break; } case TEXT_BANK_KALEIDO: { auto value = kaleidoMap[key].get(); std::string searchString = "$0"; size_t index = value.find(searchString); if (index != std::string::npos) { assert(arg != nullptr); value.replace(index, searchString.size(), std::string(arg)); return value; } else { return value; } break; } case TEXT_BANK_FILECHOOSE: { auto value = fileChooseMap[key].get(); std::string searchString = "$0"; size_t index = value.find(searchString); if (index != std::string::npos) { assert(arg != nullptr); value.replace(index, searchString.size(), std::string(arg)); return value; } else { return value; } break; } } } const char* GetLanguageCode() { switch (CVarGetInteger("gLanguages", 0)) { case LANGUAGE_FRA: return "fr-FR"; break; case LANGUAGE_GER: return "de-DE"; break; } return "en-US"; } // MARK: - Boss Title Cards std::string NameForSceneId(int16_t sceneId) { auto key = std::to_string(sceneId); auto name = GetParameritizedText(key, TEXT_BANK_SCENES, nullptr); return name; } static std::string titleCardText; void RegisterOnSceneInitHook() { GameInteractor::Instance->RegisterGameHook([](int16_t sceneNum) { if (!CVarGetInteger("gA11yTTS", 0)) return; titleCardText = NameForSceneId(sceneNum); }); } void RegisterOnPresentTitleCardHook() { GameInteractor::Instance->RegisterGameHook([]() { if (!CVarGetInteger("gA11yTTS", 0)) return; SpeechSynthesizer::Instance->Speak(titleCardText.c_str(), GetLanguageCode()); }); } // MARK: - Interface Updates void RegisterOnInterfaceUpdateHook() { GameInteractor::Instance->RegisterGameHook([]() { if (!CVarGetInteger("gA11yTTS", 0)) return; static uint32_t prevTimer = 0; static char ttsAnnounceBuf[32]; uint32_t timer = 0; if (gSaveContext.timer1State != 0) { timer = gSaveContext.timer1Value; } else if (gSaveContext.timer2State != 0) { timer = gSaveContext.timer2Value; } if (timer > 0) { if (timer > prevTimer || (timer % 30 == 0 && prevTimer != timer)) { uint32_t minutes = timer / 60; uint32_t seconds = timer % 60; char* announceBuf = ttsAnnounceBuf; char arg[8]; // at least big enough where no s8 string will overflow if (minutes > 0) { snprintf(arg, sizeof(arg), "%d", minutes); auto translation = GetParameritizedText((minutes > 1) ? "minutes_plural" : "minutes_singular", TEXT_BANK_MISC, arg); announceBuf += snprintf(announceBuf, sizeof(ttsAnnounceBuf), "%s ", translation.c_str()); } if (seconds > 0) { snprintf(arg, sizeof(arg), "%d", seconds); auto translation = GetParameritizedText((seconds > 1) ? "seconds_plural" : "seconds_singular", TEXT_BANK_MISC, arg); announceBuf += snprintf(announceBuf, sizeof(ttsAnnounceBuf), "%s", translation.c_str()); } assert(announceBuf < ttsAnnounceBuf + sizeof(ttsAnnounceBuf)); SpeechSynthesizer::Instance->Speak(ttsAnnounceBuf, GetLanguageCode()); prevTimer = timer; } } prevTimer = timer; if (!GameInteractor::IsSaveLoaded(true)) return; static int16_t lostHealth = 0; static int16_t prevHealth = 0; if (gSaveContext.health - prevHealth < 0) { lostHealth += prevHealth - gSaveContext.health; } if (gPlayState->state.frames % 7 == 0) { if (lostHealth >= 16) { Audio_PlaySoundGeneral(NA_SE_SY_CANCEL, &D_801333D4, 4, &D_801333E0, &D_801333E0, &D_801333E8); lostHealth -= 16; } } prevHealth = gSaveContext.health; }); } void RegisterOnKaleidoscopeUpdateHook() { GameInteractor::Instance->RegisterGameHook([](int16_t inDungeonScene) { if (!CVarGetInteger("gA11yTTS", 0)) return; static int16_t prevCursorIndex = 0; static uint16_t prevCursorSpecialPos = 0; static uint16_t prevCursorPoint[5] = { 0 }; static int16_t prevPromptChoice = -1; static int16_t prevSubState = -1; static int16_t prevState = -1; PauseContext* pauseCtx = &gPlayState->pauseCtx; Input* input = &gPlayState->state.input[0]; // Save game prompt if (pauseCtx->state == 7) { if (pauseCtx->unk_1EC == 1) { // prompt if (prevPromptChoice != pauseCtx->promptChoice) { auto prompt = GetParameritizedText(pauseCtx->promptChoice == 0 ? "yes" : "no", TEXT_BANK_MISC, nullptr); if (prevPromptChoice == -1) { auto translation = GetParameritizedText("save_prompt", TEXT_BANK_KALEIDO, nullptr); SpeechSynthesizer::Instance->Speak((translation + " - " + prompt).c_str(), GetLanguageCode()); } else { SpeechSynthesizer::Instance->Speak(prompt.c_str(), GetLanguageCode()); } prevPromptChoice = pauseCtx->promptChoice; } } else if (pauseCtx->unk_1EC == 4 && prevSubState != 4) { // Saved auto translation = GetParameritizedText("game_saved", TEXT_BANK_KALEIDO, nullptr); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); } prevSubState = pauseCtx->unk_1EC; prevState = pauseCtx->state; return; } // Game over + prompts if (pauseCtx->state >= 0xC && pauseCtx->state <= 0x10) { // Reset prompt tracker after state change if (prevState != pauseCtx->state) { prevPromptChoice = -1; } switch (pauseCtx->state) { // Game over in full alpha case 0xC: { // Fire once on state change if (prevState != pauseCtx->state) { auto translation = GetParameritizedText("game_over", TEXT_BANK_KALEIDO, nullptr); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); } break; } // Prompt for save case 0xE: { if (prevPromptChoice != pauseCtx->promptChoice) { auto prompt = GetParameritizedText(pauseCtx->promptChoice == 0 ? "yes" : "no", TEXT_BANK_MISC, nullptr); if (prevPromptChoice == -1) { auto translation = GetParameritizedText("save_prompt", TEXT_BANK_KALEIDO, nullptr); SpeechSynthesizer::Instance->Speak((translation + " - " + prompt).c_str(), GetLanguageCode()); } else { SpeechSynthesizer::Instance->Speak(prompt.c_str(), GetLanguageCode()); } prevPromptChoice = pauseCtx->promptChoice; } break; } // Game saved case 0xF: { // Fire once on state change if (prevState != pauseCtx->state) { auto translation = GetParameritizedText("game_saved", TEXT_BANK_KALEIDO, nullptr); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); } break; } // Prompt to continue playing case 0x10: { if (prevPromptChoice != pauseCtx->promptChoice) { auto prompt = GetParameritizedText(pauseCtx->promptChoice == 0 ? "yes" : "no", TEXT_BANK_MISC, nullptr); if (prevPromptChoice == -1) { auto translation = GetParameritizedText("continue_game", TEXT_BANK_KALEIDO, nullptr); SpeechSynthesizer::Instance->Speak((translation + " - " + prompt).c_str(), GetLanguageCode()); } else { SpeechSynthesizer::Instance->Speak(prompt.c_str(), GetLanguageCode()); } prevPromptChoice = pauseCtx->promptChoice; } break; } } prevState = pauseCtx->state; return; } // Announce page when // Kaleido pages are rotating and page halfway rotated // Or Kaleido was just opened if ((pauseCtx->unk_1E4 == 1 && pauseCtx->unk_1EA == 32) || (pauseCtx->state == 4 && prevState != 4)) { uint16_t modeNextPageMap[] = { PAUSE_MAP, PAUSE_EQUIP, PAUSE_QUEST, PAUSE_ITEM, PAUSE_EQUIP, PAUSE_MAP, PAUSE_ITEM, PAUSE_QUEST, }; uint16_t nextPage = modeNextPageMap[pauseCtx->mode]; switch (nextPage) { case PAUSE_ITEM: { auto translation = GetParameritizedText("item_menu", TEXT_BANK_KALEIDO, nullptr); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); break; } case PAUSE_MAP: { std::string map; if (inDungeonScene) { std::string key = std::to_string(gSaveContext.mapIndex); map = GetParameritizedText(key, TEXT_BANK_SCENES, nullptr); } else { map = GetParameritizedText("overworld", TEXT_BANK_KALEIDO, nullptr); } auto translation = GetParameritizedText("map_menu", TEXT_BANK_KALEIDO, map.c_str()); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); break; } case PAUSE_QUEST: { auto translation = GetParameritizedText("quest_menu", TEXT_BANK_KALEIDO, nullptr); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); break; } case PAUSE_EQUIP: { auto translation = GetParameritizedText("equip_menu", TEXT_BANK_KALEIDO, nullptr); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); break; } } prevState = pauseCtx->state; return; } prevState = pauseCtx->state; if (pauseCtx->state != 6) { // Reset cursor index and values so it is announced when pause is reopened prevCursorIndex = -1; prevPromptChoice = -1; prevSubState = -1; return; } if ((pauseCtx->debugState != 1) && (pauseCtx->debugState != 2)) { char arg[8]; if (CHECK_BTN_ALL(input->press.button, BTN_DUP)) { // Normalize hearts to fractional count similar to z_lifemeter int curHeartFraction = gSaveContext.health % 16; int fullHearts = gSaveContext.health / 16; float fraction = ceilf((float)curHeartFraction / 5) * 0.25; float health = (float)fullHearts + fraction; snprintf(arg, sizeof(arg), "%g", health); auto translation = GetParameritizedText("health", TEXT_BANK_KALEIDO, arg); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); } else if (CHECK_BTN_ALL(input->press.button, BTN_DLEFT) && gSaveContext.magicCapacity != 0) { // Normalize magic to percentage float magicLevel = ((float)gSaveContext.magic / gSaveContext.magicCapacity) * 100; snprintf(arg, sizeof(arg), "%.0f%%", magicLevel); auto translation = GetParameritizedText("magic", TEXT_BANK_KALEIDO, arg); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); } else if (CHECK_BTN_ALL(input->press.button, BTN_DDOWN)) { snprintf(arg, sizeof(arg), "%d", gSaveContext.rupees); auto translation = GetParameritizedText("rupees", TEXT_BANK_KALEIDO, arg); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); } else if (CHECK_BTN_ALL(input->press.button, BTN_DRIGHT)) { //TODO: announce timer? } } uint16_t cursorIndex = (pauseCtx->pageIndex == PAUSE_MAP && !inDungeonScene) ? PAUSE_WORLD_MAP : pauseCtx->pageIndex; if (prevCursorIndex == cursorIndex && prevCursorSpecialPos == pauseCtx->cursorSpecialPos && prevCursorPoint[cursorIndex] == pauseCtx->cursorPoint[cursorIndex]) { return; } prevCursorSpecialPos = pauseCtx->cursorSpecialPos; if (pauseCtx->cursorSpecialPos > 0) { return; } std::string buttonNames[] = { "input_button_c_left", "input_button_c_down", "input_button_c_right", "input_d_pad_up", "input_d_pad_down", "input_d_pad_left", "input_d_pad_right", }; int8_t assignedTo = -1; switch (pauseCtx->pageIndex) { case PAUSE_ITEM: { char arg[8]; // at least big enough where no s8 string will overflow switch (pauseCtx->cursorItem[PAUSE_ITEM]) { case ITEM_STICK: case ITEM_NUT: case ITEM_BOMB: case ITEM_BOMBCHU: case ITEM_SLINGSHOT: case ITEM_BOW: case ITEM_BEAN: snprintf(arg, sizeof(arg), "%d", AMMO(pauseCtx->cursorItem[PAUSE_ITEM])); break; default: arg[0] = '\0'; } if (pauseCtx->cursorItem[PAUSE_ITEM] == PAUSE_ITEM_NONE || pauseCtx->cursorItem[PAUSE_ITEM] == ITEM_NONE) { prevCursorIndex = -1; return; } std::string key = std::to_string(pauseCtx->cursorItem[PAUSE_ITEM]); std::string itemTranslation = GetParameritizedText(key, TEXT_BANK_KALEIDO, arg); // Check if item is assigned to a button for (size_t i = 0; i < ARRAY_COUNT(gSaveContext.equips.cButtonSlots); i++) { if (gSaveContext.equips.buttonItems[i + 1] == pauseCtx->cursorItem[PAUSE_ITEM]) { assignedTo = i; break; } } if (assignedTo != -1) { auto button = GetParameritizedText(buttonNames[assignedTo], TEXT_BANK_MISC, nullptr); auto translation = GetParameritizedText("assigned_to", TEXT_BANK_KALEIDO, button.c_str()); SpeechSynthesizer::Instance->Speak((itemTranslation + " - " + translation).c_str(), GetLanguageCode()); } else { SpeechSynthesizer::Instance->Speak(itemTranslation.c_str(), GetLanguageCode()); } break; } case PAUSE_MAP: if (inDungeonScene) { // Dungeon map items if (pauseCtx->cursorItem[PAUSE_MAP] != PAUSE_ITEM_NONE) { std::string key = std::to_string(pauseCtx->cursorItem[PAUSE_MAP]); auto translation = GetParameritizedText(key, TEXT_BANK_KALEIDO, nullptr); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); } else { // Dungeon map floor numbers char arg[8]; int cursorPoint = pauseCtx->cursorPoint[PAUSE_MAP]; // Cursor is on a dungeon floor position if (cursorPoint >= 3 && cursorPoint < 11) { int floorID = gMapData->floorID[gPlayState->interfaceCtx.unk_25A][pauseCtx->dungeonMapSlot - 3]; // Normalize so F1 == 0, and negative numbers are basement levels int normalizedFloor = (floorID * -1) + 8; if (normalizedFloor >= 0) { snprintf(arg, sizeof(arg), "%d", normalizedFloor + 1); auto translation = GetParameritizedText("floor", TEXT_BANK_KALEIDO, arg); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); } else { snprintf(arg, sizeof(arg), "%d", normalizedFloor * -1); auto translation = GetParameritizedText("basement", TEXT_BANK_KALEIDO, arg); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); } } } } else { std::string key = std::to_string(0x0100 + pauseCtx->cursorPoint[PAUSE_WORLD_MAP]); auto translation = GetParameritizedText(key, TEXT_BANK_KALEIDO, nullptr); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); } break; case PAUSE_QUEST: { char arg[8]; // at least big enough where no s8 string will overflow switch (pauseCtx->cursorItem[PAUSE_QUEST]) { case ITEM_SKULL_TOKEN: snprintf(arg, sizeof(arg), "%d", gSaveContext.inventory.gsTokens); break; case ITEM_HEART_CONTAINER: snprintf(arg, sizeof(arg), "%d", (gSaveContext.inventory.questItems & 0xF0000000) >> 0x1C); break; default: arg[0] = '\0'; } if (pauseCtx->cursorItem[PAUSE_QUEST] == PAUSE_ITEM_NONE) { prevCursorIndex = -1; return; } std::string key = std::to_string(pauseCtx->cursorItem[PAUSE_QUEST]); auto translation = GetParameritizedText(key, TEXT_BANK_KALEIDO, arg); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); break; } case PAUSE_EQUIP: { if (pauseCtx->namedItem == PAUSE_ITEM_NONE) { prevCursorIndex = -1; return; } std::string key = std::to_string(pauseCtx->cursorItem[PAUSE_EQUIP]); auto itemTranslation = GetParameritizedText(key, TEXT_BANK_KALEIDO, nullptr); uint8_t checkEquipItem = pauseCtx->namedItem; // BGS from kaleido reports as ITEM_HEART_PIECE_2 (122) // remap BGS and broken knife to be the BGS item for the current equip check if (checkEquipItem == ITEM_HEART_PIECE_2 || checkEquipItem == ITEM_SWORD_KNIFE) { checkEquipItem = ITEM_SWORD_BGS; } // Check if equipment item is currently equipped or assigned to a button if (checkEquipItem >= ITEM_SWORD_KOKIRI && checkEquipItem <= ITEM_BOOTS_HOVER) { uint8_t checkEquipType = (checkEquipItem - ITEM_SWORD_KOKIRI) / 3; uint8_t checkEquipValue = ((checkEquipItem - ITEM_SWORD_KOKIRI) % 3) + 1; if (CUR_EQUIP_VALUE(checkEquipType) == checkEquipValue) { itemTranslation = GetParameritizedText("equipped", TEXT_BANK_KALEIDO, itemTranslation.c_str()); } for (size_t i = 0; i < ARRAY_COUNT(gSaveContext.equips.cButtonSlots); i++) { if (gSaveContext.equips.buttonItems[i + 1] == checkEquipItem) { assignedTo = i; break; } } } if (assignedTo != -1) { auto button = GetParameritizedText(buttonNames[assignedTo], TEXT_BANK_MISC, nullptr); auto translation = GetParameritizedText("assigned_to", TEXT_BANK_KALEIDO, button.c_str()); SpeechSynthesizer::Instance->Speak((itemTranslation + " - " + translation).c_str(), GetLanguageCode()); } else { SpeechSynthesizer::Instance->Speak(itemTranslation.c_str(), GetLanguageCode()); } break; } default: break; } prevCursorIndex = cursorIndex; memcpy(prevCursorPoint, pauseCtx->cursorPoint, sizeof(prevCursorPoint)); }); } void RegisterOnUpdateMainMenuSelection() { GameInteractor::Instance->RegisterGameHook([]() { if (!CVarGetInteger("gA11yTTS", 0)) return; auto translation = GetParameritizedText("file1", TEXT_BANK_FILECHOOSE, nullptr); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); }); GameInteractor::Instance->RegisterGameHook([](uint16_t optionIndex) { if (!CVarGetInteger("gA11yTTS", 0)) return; switch (optionIndex) { case FS_BTN_MAIN_FILE_1: { auto translation = GetParameritizedText("file1", TEXT_BANK_FILECHOOSE, nullptr); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); break; } case FS_BTN_MAIN_FILE_2: { auto translation = GetParameritizedText("file2", TEXT_BANK_FILECHOOSE, nullptr); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); break; } case FS_BTN_MAIN_FILE_3: { auto translation = GetParameritizedText("file3", TEXT_BANK_FILECHOOSE, nullptr); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); break; } case FS_BTN_MAIN_OPTIONS: { auto translation = GetParameritizedText("options", TEXT_BANK_FILECHOOSE, nullptr); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); break; } case FS_BTN_MAIN_COPY: { auto translation = GetParameritizedText("copy", TEXT_BANK_FILECHOOSE, nullptr); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); break; } case FS_BTN_MAIN_ERASE: { auto translation = GetParameritizedText("erase", TEXT_BANK_FILECHOOSE, nullptr); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); break; } default: break; } }); GameInteractor::Instance->RegisterGameHook([](uint16_t optionIndex) { if (!CVarGetInteger("gA11yTTS", 0)) return; switch (optionIndex) { case FS_BTN_CONFIRM_YES: { auto translation = GetParameritizedText("confirm", TEXT_BANK_FILECHOOSE, nullptr); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); break; } case FS_BTN_CONFIRM_QUIT: { auto translation = GetParameritizedText("quit", TEXT_BANK_FILECHOOSE, nullptr); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); break; } default: break; } }); GameInteractor::Instance->RegisterGameHook([](uint16_t optionIndex) { if (!CVarGetInteger("gA11yTTS", 0)) return; switch (optionIndex) { case FS_BTN_COPY_FILE_1: { auto translation = GetParameritizedText("file1", TEXT_BANK_FILECHOOSE, nullptr); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); break; } case FS_BTN_COPY_FILE_2: { auto translation = GetParameritizedText("file2", TEXT_BANK_FILECHOOSE, nullptr); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); break; } case FS_BTN_COPY_FILE_3: { auto translation = GetParameritizedText("file3", TEXT_BANK_FILECHOOSE, nullptr); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); break; } case FS_BTN_COPY_QUIT: { auto translation = GetParameritizedText("quit", TEXT_BANK_FILECHOOSE, nullptr); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); break; } default: break; } }); GameInteractor::Instance->RegisterGameHook([](uint16_t optionIndex) { if (!CVarGetInteger("gA11yTTS", 0)) return; switch (optionIndex) { case FS_BTN_CONFIRM_YES: { auto translation = GetParameritizedText("confirm", TEXT_BANK_FILECHOOSE, nullptr); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); break; } case FS_BTN_CONFIRM_QUIT: { auto translation = GetParameritizedText("quit", TEXT_BANK_FILECHOOSE, nullptr); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); break; } default: break; } }); GameInteractor::Instance->RegisterGameHook([](uint16_t optionIndex) { if (!CVarGetInteger("gA11yTTS", 0)) return; switch (optionIndex) { case FS_BTN_ERASE_FILE_1: { auto translation = GetParameritizedText("file1", TEXT_BANK_FILECHOOSE, nullptr); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); break; } case FS_BTN_ERASE_FILE_2: { auto translation = GetParameritizedText("file2", TEXT_BANK_FILECHOOSE, nullptr); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); break; } case FS_BTN_ERASE_FILE_3: { auto translation = GetParameritizedText("file3", TEXT_BANK_FILECHOOSE, nullptr); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); break; } case FS_BTN_ERASE_QUIT: { auto translation = GetParameritizedText("quit", TEXT_BANK_FILECHOOSE, nullptr); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); break; } default: break; } }); GameInteractor::Instance->RegisterGameHook([](uint16_t optionIndex) { if (!CVarGetInteger("gA11yTTS", 0)) return; switch (optionIndex) { case FS_BTN_CONFIRM_YES: { auto translation = GetParameritizedText("confirm", TEXT_BANK_FILECHOOSE, nullptr); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); break; } case FS_BTN_CONFIRM_QUIT: { auto translation = GetParameritizedText("quit", TEXT_BANK_FILECHOOSE, nullptr); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); break; } default: break; } }); GameInteractor::Instance->RegisterGameHook([](uint8_t optionIndex) { if (!CVarGetInteger("gA11yTTS", 0)) return; switch (optionIndex) { case FS_AUDIO_STEREO: { auto translation = GetParameritizedText("audio_stereo", TEXT_BANK_FILECHOOSE, nullptr); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); break; } case FS_AUDIO_MONO: { auto translation = GetParameritizedText("audio_mono", TEXT_BANK_FILECHOOSE, nullptr); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); break; } case FS_AUDIO_HEADSET: { auto translation = GetParameritizedText("audio_headset", TEXT_BANK_FILECHOOSE, nullptr); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); break; } case FS_AUDIO_SURROUND: { auto translation = GetParameritizedText("audio_surround", TEXT_BANK_FILECHOOSE, nullptr); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); break; } default: break; } }); GameInteractor::Instance->RegisterGameHook([](uint8_t optionIndex) { if (!CVarGetInteger("gA11yTTS", 0)) return; switch (optionIndex) { case FS_TARGET_SWITCH: { auto translation = GetParameritizedText("target_switch", TEXT_BANK_FILECHOOSE, nullptr); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); break; } case FS_TARGET_HOLD: { auto translation = GetParameritizedText("target_hold", TEXT_BANK_FILECHOOSE, nullptr); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); break; } default: break; } }); GameInteractor::Instance->RegisterGameHook([](uint8_t optionIndex) { if (!CVarGetInteger("gA11yTTS", 0)) return; switch (optionIndex) { case LANGUAGE_ENG: { auto translation = GetParameritizedText("language_english", TEXT_BANK_FILECHOOSE, nullptr); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); break; } case LANGUAGE_GER: { auto translation = GetParameritizedText("language_german", TEXT_BANK_FILECHOOSE, nullptr); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); break; } case LANGUAGE_FRA: { auto translation = GetParameritizedText("language_french", TEXT_BANK_FILECHOOSE, nullptr); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); break; } default: break; } }); GameInteractor::Instance->RegisterGameHook([](uint8_t questIndex) { if (!CVarGetInteger("gA11yTTS", 0)) return; switch (questIndex) { case QUEST_NORMAL: { auto translation = GetParameritizedText("quest_sel_vanilla", TEXT_BANK_FILECHOOSE, nullptr); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); break; } case QUEST_MASTER: { auto translation = GetParameritizedText("quest_sel_mq", TEXT_BANK_FILECHOOSE, nullptr); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); break; } case QUEST_RANDOMIZER: { auto translation = GetParameritizedText("quest_sel_randomizer", TEXT_BANK_FILECHOOSE, nullptr); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); break; } case QUEST_BOSSRUSH: { auto translation = GetParameritizedText("quest_sel_boss_rush", TEXT_BANK_FILECHOOSE, nullptr); SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); break; } default: break; } }); GameInteractor::Instance->RegisterGameHook([](uint8_t optionIndex, uint8_t optionValue) { if (!CVarGetInteger("gA11yTTS", 0)) return; auto optionName = BossRush_GetSettingName(optionIndex, gSaveContext.language); auto optionValueName = BossRush_GetSettingChoiceName(optionIndex, optionValue, gSaveContext.language); auto translation = optionName + std::string(" - ") + optionValueName; SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); }); GameInteractor::Instance->RegisterGameHook([](int16_t charCode) { if (!CVarGetInteger("gA11yTTS", 0)) return; char charVal[2]; std::string translation; if (charCode < 10) { // Digits sprintf(charVal, "%c", charCode + 0x30); } else if (charCode >= 10 && charCode < 36) { // Uppercase letters sprintf(charVal, "%c", charCode + 0x37); translation = GetParameritizedText("capital_letter", TEXT_BANK_FILECHOOSE, charVal); } else if (charCode >= 36 && charCode < 62) { // Lowercase letters sprintf(charVal, "%c", charCode + 0x3D); } else if (charCode == 62) { // Space translation = GetParameritizedText("space", TEXT_BANK_FILECHOOSE, nullptr); } else if (charCode == 63) { // - translation = GetParameritizedText("hyphen", TEXT_BANK_FILECHOOSE, nullptr); } else if (charCode == 64) { // . translation = GetParameritizedText("period", TEXT_BANK_FILECHOOSE, nullptr); } else if (charCode == 0xF0 + FS_KBD_BTN_BACKSPACE) { translation = GetParameritizedText("backspace", TEXT_BANK_FILECHOOSE, nullptr); } else if (charCode == 0xF0 + FS_KBD_BTN_END) { translation = GetParameritizedText("end", TEXT_BANK_FILECHOOSE, nullptr); } else { sprintf(charVal, "%c", charCode); } if (translation.empty()) { SpeechSynthesizer::Instance->Speak(charVal, GetLanguageCode()); } else { SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); } }); } // MARK: - Dialog Messages static uint8_t ttsHasMessage; static uint8_t ttsHasNewMessage; static int8_t ttsCurrentHighlightedChoice; std::string remap(uint8_t character) { switch (character) { case 0x80: return "À"; case 0x81: return "î"; case 0x82: return "Â"; case 0x83: return "Ä"; case 0x84: return "Ç"; case 0x85: return "È"; case 0x86: return "É"; case 0x87: return "Ê"; case 0x88: return "Ë"; case 0x89: return "Ï"; case 0x8A: return "Ô"; case 0x8B: return "Ö"; case 0x8C: return "Ù"; case 0x8D: return "Û"; case 0x8E: return "Ü"; case 0x8F: return "ß"; case 0x90: return "à"; case 0x91: return "á"; case 0x92: return "â"; case 0x93: return "ä"; case 0x94: return "ç"; case 0x95: return "è"; case 0x96: return "é"; case 0x97: return "ê"; case 0x98: return "ë"; case 0x99: return "ï"; case 0x9A: return "ô"; case 0x9B: return "ö"; case 0x9C: return "ù"; case 0x9D: return "û"; case 0x9E: return "ü"; case 0x9F: return GetParameritizedText("input_button_a", TEXT_BANK_MISC, nullptr); case 0xA0: return GetParameritizedText("input_button_b", TEXT_BANK_MISC, nullptr); case 0xA1: return GetParameritizedText("input_button_c", TEXT_BANK_MISC, nullptr); case 0xA2: return GetParameritizedText("input_button_l", TEXT_BANK_MISC, nullptr); case 0xA3: return GetParameritizedText("input_button_r", TEXT_BANK_MISC, nullptr); case 0xA4: return GetParameritizedText("input_button_z", TEXT_BANK_MISC, nullptr); case 0xA5: return GetParameritizedText("input_button_c_up", TEXT_BANK_MISC, nullptr); case 0xA6: return GetParameritizedText("input_button_c_down", TEXT_BANK_MISC, nullptr); case 0xA7: return GetParameritizedText("input_button_c_left", TEXT_BANK_MISC, nullptr); case 0xA8: return GetParameritizedText("input_button_c_right", TEXT_BANK_MISC, nullptr); case 0xAA: return GetParameritizedText("input_analog_stick", TEXT_BANK_MISC, nullptr); case 0xAB: return GetParameritizedText("input_d_pad", TEXT_BANK_MISC, nullptr); default: return ""; } } std::string Message_TTS_Decode(uint8_t* sourceBuf, uint16_t startOfset, uint16_t size) { std::string output; uint32_t destWriteIndex = 0; uint8_t isListingChoices = 0; for (uint16_t i = 0; i < size; i++) { uint8_t cchar = sourceBuf[i + startOfset]; if (cchar < ' ') { switch (cchar) { case MESSAGE_NEWLINE: output += (isListingChoices) ? '\n' : ' '; break; case MESSAGE_THREE_CHOICE: case MESSAGE_TWO_CHOICE: output += '\n'; isListingChoices = 1; break; case MESSAGE_COLOR: case MESSAGE_SHIFT: case MESSAGE_TEXT_SPEED: case MESSAGE_BOX_BREAK_DELAYED: case MESSAGE_FADE: case MESSAGE_ITEM_ICON: i++; break; case MESSAGE_FADE2: case MESSAGE_SFX: case MESSAGE_TEXTID: i += 2; break; default: break; } } else { if (cchar <= 0x80) { output += cchar; } else { output += remap(cchar); } } } return output; } void RegisterOnDialogMessageHook() { GameInteractor::Instance->RegisterGameHook([]() { if (!CVarGetInteger("gA11yTTS", 0)) return; MessageContext *msgCtx = &gPlayState->msgCtx; if (msgCtx->msgMode == MSGMODE_TEXT_NEXT_MSG || msgCtx->msgMode == MSGMODE_DISPLAY_SONG_PLAYED_TEXT_BEGIN || (msgCtx->msgMode == MSGMODE_TEXT_CONTINUING && msgCtx->stateTimer == 1)) { ttsHasNewMessage = 1; } else if (msgCtx->msgMode == MSGMODE_TEXT_DISPLAYING || msgCtx->msgMode == MSGMODE_TEXT_AWAIT_NEXT || msgCtx->msgMode == MSGMODE_TEXT_DONE || msgCtx->msgMode == MSGMODE_TEXT_DELAYED_BREAK || msgCtx->msgMode == MSGMODE_OCARINA_STARTING || msgCtx->msgMode == MSGMODE_OCARINA_PLAYING || msgCtx->msgMode == MSGMODE_DISPLAY_SONG_PLAYED_TEXT || msgCtx->msgMode == MSGMODE_DISPLAY_SONG_PLAYED_TEXT || msgCtx->msgMode == MSGMODE_SONG_PLAYED_ACT_BEGIN || msgCtx->msgMode == MSGMODE_SONG_PLAYED_ACT || msgCtx->msgMode == MSGMODE_SONG_PLAYBACK_STARTING || msgCtx->msgMode == MSGMODE_SONG_PLAYBACK || msgCtx->msgMode == MSGMODE_SONG_DEMONSTRATION_STARTING || msgCtx->msgMode == MSGMODE_SONG_DEMONSTRATION_SELECT_INSTRUMENT || msgCtx->msgMode == MSGMODE_SONG_DEMONSTRATION ) { if (ttsHasNewMessage) { ttsHasMessage = 1; ttsHasNewMessage = 0; ttsCurrentHighlightedChoice = 0; uint16_t size = msgCtx->decodedTextLen; auto decodedMsg = Message_TTS_Decode(msgCtx->msgBufDecoded, 0, size); SpeechSynthesizer::Instance->Speak(decodedMsg.c_str(), GetLanguageCode()); } else if (msgCtx->msgMode == MSGMODE_TEXT_DONE && msgCtx->choiceNum > 0 && msgCtx->choiceIndex != ttsCurrentHighlightedChoice) { ttsCurrentHighlightedChoice = msgCtx->choiceIndex; uint16_t startOffset = 0; while (startOffset < msgCtx->decodedTextLen) { if (msgCtx->msgBufDecoded[startOffset] == MESSAGE_TWO_CHOICE || msgCtx->msgBufDecoded[startOffset] == MESSAGE_THREE_CHOICE) { startOffset++; break; } startOffset++; } uint16_t endOffset = 0; if (startOffset < msgCtx->decodedTextLen) { uint8_t i = msgCtx->choiceIndex; while (i-- > 0) { while (startOffset < msgCtx->decodedTextLen) { if (msgCtx->msgBufDecoded[startOffset] == MESSAGE_NEWLINE) { startOffset++; break; } startOffset++; } } endOffset = startOffset; while (endOffset < msgCtx->decodedTextLen) { if (msgCtx->msgBufDecoded[endOffset] == MESSAGE_NEWLINE) { break; } endOffset++; } if (startOffset < msgCtx->decodedTextLen && startOffset != endOffset) { uint16_t size = endOffset - startOffset; auto decodedMsg = Message_TTS_Decode(msgCtx->msgBufDecoded, startOffset, size); SpeechSynthesizer::Instance->Speak(decodedMsg.c_str(), GetLanguageCode()); } } } } else if (ttsHasMessage) { ttsHasMessage = 0; ttsHasNewMessage = 0; if (msgCtx->decodedTextLen < 3 || (msgCtx->msgBufDecoded[msgCtx->decodedTextLen - 2] != MESSAGE_FADE && msgCtx->msgBufDecoded[msgCtx->decodedTextLen - 3] != MESSAGE_FADE2)) { SpeechSynthesizer::Instance->Speak("", GetLanguageCode()); // cancel current speech (except for faded out messages) } } }); } // MARK: - Main Registration void InitTTSBank() { std::string languageSuffix = "_eng.json"; switch (CVarGetInteger("gLanguages", 0)) { case LANGUAGE_FRA: languageSuffix = "_fra.json"; break; case LANGUAGE_GER: languageSuffix = "_ger.json"; break; } auto sceneFile = LUS::Context::GetInstance()->GetResourceManager()->GetArchiveManager()->LoadFileRaw("accessibility/texts/scenes" + languageSuffix); if (sceneFile != nullptr) { sceneMap = nlohmann::json::parse(*sceneFile->Buffer.get(), nullptr, true, true); } auto miscFile = LUS::Context::GetInstance()->GetResourceManager()->GetArchiveManager()->LoadFileRaw("accessibility/texts/misc" + languageSuffix); if (miscFile != nullptr) { miscMap = nlohmann::json::parse(*miscFile->Buffer.get(), nullptr, true, true); } auto kaleidoFile = LUS::Context::GetInstance()->GetResourceManager()->GetArchiveManager()->LoadFileRaw("accessibility/texts/kaleidoscope" + languageSuffix); if (kaleidoFile != nullptr) { kaleidoMap = nlohmann::json::parse(*kaleidoFile->Buffer.get(), nullptr, true, true); } auto fileChooseFile = LUS::Context::GetInstance()->GetResourceManager()->GetArchiveManager()->LoadFileRaw("accessibility/texts/filechoose" + languageSuffix); if (fileChooseFile != nullptr) { fileChooseMap = nlohmann::json::parse(*fileChooseFile->Buffer.get(), nullptr, true, true); } } void RegisterOnSetGameLanguageHook() { GameInteractor::Instance->RegisterGameHook([]() { InitTTSBank(); }); } void RegisterTTSModHooks() { RegisterOnSetGameLanguageHook(); RegisterOnDialogMessageHook(); RegisterOnSceneInitHook(); RegisterOnPresentTitleCardHook(); RegisterOnInterfaceUpdateHook(); RegisterOnKaleidoscopeUpdateHook(); RegisterOnUpdateMainMenuSelection(); } void RegisterTTS() { InitTTSBank(); RegisterTTSModHooks(); }