From 21466192e553a30d81aded71589bde863d37bf68 Mon Sep 17 00:00:00 2001 From: David Chavez Date: Thu, 2 Mar 2023 09:27:28 +0100 Subject: [PATCH] [Accessibility] Text to Speech (#2487) --- OTRExporter/OTRExporter/Main.cpp | 15 +- .../accessibility/texts/filechoose_eng.json | 16 + .../accessibility/texts/filechoose_fra.json | 16 + .../accessibility/texts/filechoose_ger.json | 16 + .../accessibility/texts/kaleidoscope_eng.json | 219 ++++++ .../accessibility/texts/kaleidoscope_fra.json | 219 ++++++ .../accessibility/texts/kaleidoscope_ger.json | 219 ++++++ .../assets/accessibility/texts/misc_eng.json | 18 + .../assets/accessibility/texts/misc_fra.json | 18 + .../assets/accessibility/texts/misc_ger.json | 18 + .../accessibility/texts/scenes_eng.json | 112 +++ .../accessibility/texts/scenes_fra.json | 112 +++ .../accessibility/texts/scenes_ger.json | 112 +++ soh/CMakeLists.txt | 33 +- .../game-interactor/GameInteractor.h | 21 +- .../game-interactor/GameInteractor_Hooks.cpp | 66 +- .../game-interactor/GameInteractor_Hooks.h | 23 +- soh/soh/Enhancements/mods.cpp | 2 + .../DarwinSpeechSynthesizer.h | 27 + .../DarwinSpeechSynthesizer.mm | 33 + .../SAPISpeechSynthesizer.cpp | 56 ++ .../speechsynthesizer/SAPISpeechSynthesizer.h | 25 + .../speechsynthesizer/SpeechSynthesizer.cpp | 32 + .../speechsynthesizer/SpeechSynthesizer.h | 38 + soh/soh/Enhancements/tts/tts.cpp | 724 ++++++++++++++++++ soh/soh/Enhancements/tts/tts.h | 6 + soh/soh/GameMenuBar.cpp | 18 +- soh/soh/OTRGlobals.cpp | 20 +- soh/soh/UIWidgets.cpp | 6 +- soh/soh/UIWidgets.hpp | 2 +- soh/src/code/z_actor.c | 6 +- soh/src/code/z_message_PAL.c | 2 + soh/src/code/z_parameter.c | 2 + .../gamestates/ovl_file_choose/file_choose.h | 9 +- .../ovl_file_choose/z_file_choose.c | 8 + .../ovl_file_choose/z_file_copy_erase.c | 22 + .../ovl_file_choose/z_file_nameset_PAL.c | 14 + .../ovl_kaleido_scope/z_kaleido_scope_PAL.c | 2 + 38 files changed, 2279 insertions(+), 28 deletions(-) create mode 100644 OTRExporter/assets/accessibility/texts/filechoose_eng.json create mode 100644 OTRExporter/assets/accessibility/texts/filechoose_fra.json create mode 100644 OTRExporter/assets/accessibility/texts/filechoose_ger.json create mode 100644 OTRExporter/assets/accessibility/texts/kaleidoscope_eng.json create mode 100644 OTRExporter/assets/accessibility/texts/kaleidoscope_fra.json create mode 100644 OTRExporter/assets/accessibility/texts/kaleidoscope_ger.json create mode 100644 OTRExporter/assets/accessibility/texts/misc_eng.json create mode 100644 OTRExporter/assets/accessibility/texts/misc_fra.json create mode 100644 OTRExporter/assets/accessibility/texts/misc_ger.json create mode 100644 OTRExporter/assets/accessibility/texts/scenes_eng.json create mode 100644 OTRExporter/assets/accessibility/texts/scenes_fra.json create mode 100644 OTRExporter/assets/accessibility/texts/scenes_ger.json create mode 100644 soh/soh/Enhancements/speechsynthesizer/DarwinSpeechSynthesizer.h create mode 100644 soh/soh/Enhancements/speechsynthesizer/DarwinSpeechSynthesizer.mm create mode 100644 soh/soh/Enhancements/speechsynthesizer/SAPISpeechSynthesizer.cpp create mode 100644 soh/soh/Enhancements/speechsynthesizer/SAPISpeechSynthesizer.h create mode 100644 soh/soh/Enhancements/speechsynthesizer/SpeechSynthesizer.cpp create mode 100644 soh/soh/Enhancements/speechsynthesizer/SpeechSynthesizer.h create mode 100644 soh/soh/Enhancements/tts/tts.cpp create mode 100644 soh/soh/Enhancements/tts/tts.h diff --git a/OTRExporter/OTRExporter/Main.cpp b/OTRExporter/OTRExporter/Main.cpp index 206ea7171..47a1752aa 100644 --- a/OTRExporter/OTRExporter/Main.cpp +++ b/OTRExporter/OTRExporter/Main.cpp @@ -141,14 +141,21 @@ static void ExporterProgramEnd() } } + if(item.find("accessibility") != std::string::npos) { + std::string extension = splitPath.at(splitPath.size() - 1); + splitPath.pop_back(); + if(extension == "json"){ + auto fileData = File::ReadAllBytes(item); + printf("Adding accessibility texts %s\n", StringHelper::Split(item, "texts/")[1].c_str()); + otrArchive->AddFile(StringHelper::Split(item, "Extract/assets/")[1], (uintptr_t)fileData.data(), fileData.size()); + } + continue; + } + auto fileData = File::ReadAllBytes(item); printf("otrArchive->AddFile(%s)\n", StringHelper::Split(item, "Extract/")[1].c_str()); otrArchive->AddFile(StringHelper::Split(item, "Extract/")[1], (uintptr_t)fileData.data(), fileData.size()); } - - //otrArchive->AddFile("Audiobank", (uintptr_t)Globals::Instance->GetBaseromFile("Audiobank").data(), Globals::Instance->GetBaseromFile("Audiobank").size()); - //otrArchive->AddFile("Audioseq", (uintptr_t)Globals::Instance->GetBaseromFile("Audioseq").data(), Globals::Instance->GetBaseromFile("Audioseq").size()); - //otrArchive->AddFile("Audiotable", (uintptr_t)Globals::Instance->GetBaseromFile("Audiotable").data(), Globals::Instance->GetBaseromFile("Audiotable").size()); } } diff --git a/OTRExporter/assets/accessibility/texts/filechoose_eng.json b/OTRExporter/assets/accessibility/texts/filechoose_eng.json new file mode 100644 index 000000000..ed70b4a4b --- /dev/null +++ b/OTRExporter/assets/accessibility/texts/filechoose_eng.json @@ -0,0 +1,16 @@ +{ + "file1": "File 1", + "file2": "File 2", + "file3": "File 3", + "options": "Options", + "copy": "Copy", + "erase": "Erase", + "quit": "Quit", + "confirm": "Yes", + "audio_stereo": "Sound - Stereo", + "audio_mono": "Sound - Mono", + "audio_headset": "Sound - Headset", + "audio_surround": "Sound - Surround", + "target_switch": "Targetting Mode - Switch", + "target_hold": "Targetting Mode - Hold" +} \ No newline at end of file diff --git a/OTRExporter/assets/accessibility/texts/filechoose_fra.json b/OTRExporter/assets/accessibility/texts/filechoose_fra.json new file mode 100644 index 000000000..4e601ab3d --- /dev/null +++ b/OTRExporter/assets/accessibility/texts/filechoose_fra.json @@ -0,0 +1,16 @@ +{ + "file1": "Fichier 1", + "file2": "Fichier 2", + "file3": "Fichier 3", + "options": "Options", + "copy": "Copier", + "erase": "Effacer", + "quit": "Retour", + "confirm": "Oui", + "audio_stereo": "Son - Stéréo", + "audio_mono": "Son - Mono", + "audio_headset": "Son - Casque", + "audio_surround": "Son - Surround", + "target_switch": "Visée - Fixe", + "target_hold": "Visée - Maintenue" +} \ No newline at end of file diff --git a/OTRExporter/assets/accessibility/texts/filechoose_ger.json b/OTRExporter/assets/accessibility/texts/filechoose_ger.json new file mode 100644 index 000000000..539466669 --- /dev/null +++ b/OTRExporter/assets/accessibility/texts/filechoose_ger.json @@ -0,0 +1,16 @@ +{ + "file1": "Datei 1", + "file2": "Datei 2", + "file3": "Datei 3", + "options": "Optionen", + "copy": "Kopieren", + "erase": "Löschen", + "quit": "Zurück", + "confirm": "Ja", + "audio_stereo": "Sound - Stereo", + "audio_mono": "Sound - Mono", + "audio_headset": "Sound - Kopfhörer", + "audio_surround": "Sound - Surround", + "target_switch": "Zielerfassung - Einmal drücken", + "target_hold": "Zielerfassung - Trigger halten" +} \ No newline at end of file diff --git a/OTRExporter/assets/accessibility/texts/kaleidoscope_eng.json b/OTRExporter/assets/accessibility/texts/kaleidoscope_eng.json new file mode 100644 index 000000000..26112a5eb --- /dev/null +++ b/OTRExporter/assets/accessibility/texts/kaleidoscope_eng.json @@ -0,0 +1,219 @@ +{ + "health": "health $0", + "magic": "magic $0", + "rupees": "rupees $0", + "0": "Deku Stick $0", + "1": "Deku Nut $0", + "2": "Bomb $0", + "3": "Fairy Bow $0", + "4": "Fire Arrow", + "5": "Din's Fire", + "6": "Fairy Slingshot $0", + "7": "Fairy Ocarina", + "8": "Ocarina of Time", + "9": "Bombchu $0", + "10": "Hookshot", + "11": "Longshot", + "12": "Ice Arrow", + "13": "Farore's Wind", + "14": "Boomerang", + "15": "Lens of Truth", + "16": "Magic Beans $0", + "17": "Megaton Hammer", + "18": "Light Arrow", + "19": "Nayru's Love", + "20": "Empty Bottle", + "21": "Red Potion", + "22": "Green Potion", + "23": "Blue Potion", + "24": "Fairy", + "25": "Fish", + "26": "Milk Bottle", + "27": "Ruto's Letter", + "28": "Blue Fire", + "29": "Bugs", + "30": "Big Poe", + "31": "Milk Bottle (Half)", + "32": "Poe", + "33": "Weird Egg", + "34": "Chicken", + "35": "Zelda's Letter", + "36": "Keaton Mask", + "37": "Skull Mask", + "38": "Spooky Mask", + "39": "Bunny Mask", + "40": "Goron Mask", + "41": "Zora Mask", + "42": "Gerudo Mask", + "43": "Mask of Truth", + "44": "Sold Out", + "45": "Pocket Egg", + "46": "Pocket Cucco", + "47": "Cojiro", + "48": "Odd Mushroom", + "49": "Odd Potion", + "50": "Saw", + "51": "Broken Sword", + "52": "Prescription", + "53": "Eyeball Frog", + "54": "Eyedrops", + "55": "Claim Check", + "56": "Bow Fire Arrow", + "57": "Bow Ice Arrow", + "58": "Bow Light Arrow", + "59": "Kokiri Sword", + "60": "Master Sword", + "61": "Giant's Knife", + "62": "Deku Shield", + "63": "Hylian Shield", + "64": "Mirror Shield", + "65": "Kokiri Tunic", + "66": "Goron Tunic", + "67": "Zora Tunic", + "68": "Kokiri Boots", + "69": "Iron Boots", + "70": "Hover Boots", + "71": "Bullet Bag (Holds 30)", + "72": "Bullet Bag (Holds 40)", + "73": "Bullet Bag (Holds 50)", + "74": "Quiver (Holds 30)", + "75": "Quiver (Holds 40)", + "76": "Quiver (Holds 50)", + "77": "Bomb Bag (Holds 20)", + "78": "Bomb Bag (Holds 30)", + "79": "Bomb Bag (Holds 40)", + "80": "Goron's Bracelet", + "81": "Silver Gauntlets", + "82": "Golden Gauntlets", + "83": "Silver Scale", + "84": "Golden Scale", + "85": "Giant's Knife (Broken)", + "86": "WALLET ADULT", + "87": "Giant's Wallet", + "88": "Deku Seeds", + "89": "Fishing Pole", + "90": "Minuet of Forest", + "91": "Bolero of Fire", + "92": "Serenade of Water", + "93": "Requiem of Spirit", + "94": "Nocturne of Shadow", + "95": "Prelude of Light", + "96": "Zelda's Lullaby", + "97": "Epona's Song", + "98": "Saria's Song", + "99": "Sun's Song", + "100": "Song of Time", + "101": "Song of Storms", + "102": "Forest Medallion", + "103": "Fire Medallion", + "104": "Water Medallion", + "105": "Spirit Medallion", + "106": "Shadow Medallion", + "107": "Light Medallion", + "108": "Kokiri's Emerald", + "109": "Goron's Ruby", + "110": "Zora Sapphire", + "111": "Stone of Agony", + "112": "Gerudo's Card", + "113": "Skulltula Token $0", + "114": "Heart Container $0", + "115": "Piece of Heart", + "116": "Boss Key", + "117": "Compass", + "118": "Dungeon Map", + "119": "Small Key", + "120": "MAGIC SMALL", + "121": "MAGIC LARGE", + "122": "PIECE OF HEART 2", + "123": "INVALID 1", + "124": "INVALID 2", + "125": "INVALID 3", + "126": "INVALID 4", + "127": "INVALID 5", + "128": "INVALID 6", + "129": "INVALID 7", + "130": "Milk", + "131": "Recovery Heart", + "132": "Green Rupee", + "133": "Blue Rupee", + "134": "Red Rupee", + "135": "Purple Rupee", + "136": "Gold Rupee", + "137": "INVALID 8", + "138": "STICKS 5", + "139": "STICKS 10", + "140": "NUTS 5", + "141": "NUTS 10", + "142": "BOMBS 5", + "143": "BOMBS 10", + "144": "BOMBS 20", + "145": "BOMBS 30", + "146": "ARROWS SMALL", + "147": "ARROWS MEDIUM", + "148": "ARROWS LARGE", + "149": "SEEDS 30", + "150": "BOMBCHUS 5", + "151": "BOMBCHUS 20", + "152": "STICK UPGRADE 20", + "153": "STICK UPGRADE 30", + "154": "NUT UPGRADE 30", + "155": "NUT UPGRADE 40", + "256": "Haunted Wasteland", + "257": "Gerudos Fortress", + "258": "Gerudo Valley", + "259": "Hylia Lakeside", + "260": "Lon Lon Ranch", + "261": "Market", + "262": "Hyrule Field", + "263": "Death Mountain", + "264": "Kakariko Village", + "265": "Lost Woods", + "266": "Kokiri Forest", + "267": "Zoras Domain", + "268": "", + "269": "", + "270": "", + "271": "", + "272": "", + "273": "", + "274": "", + "275": "", + "276": "", + "277": "", + "278": "", + "279": "", + "280": "", + "281": "", + "282": "", + "283": "", + "284": "", + "285": "", + "286": "", + "287": "", + "288": "", + "289": "", + "290": "", + "291": "", + "292": "Hyrule Field", + "293": "Kakariko Village", + "294": "Graveyard", + "295": "Zoras River", + "296": "Kokiri Forest", + "297": "Sacred Forest Meadow", + "298": "Lake Hylia", + "299": "Zoras Domain", + "300": "Zoras Fountain", + "301": "Gerudo Valley", + "302": "Lost Woods", + "303": "Desert Colossus", + "304": "Gerudo's Fortress", + "305": "Haunted Wasteland", + "306": "Market", + "307": "Hyrule Castle", + "308": "Death Mountain Trail", + "309": "Death Mountain Crater", + "310": "Goron City", + "311": "Lon Lon Ranch", + "312": "Question Mark", + "313": "Ganon's Castle" +} \ No newline at end of file diff --git a/OTRExporter/assets/accessibility/texts/kaleidoscope_fra.json b/OTRExporter/assets/accessibility/texts/kaleidoscope_fra.json new file mode 100644 index 000000000..b7b955cc1 --- /dev/null +++ b/OTRExporter/assets/accessibility/texts/kaleidoscope_fra.json @@ -0,0 +1,219 @@ +{ + "health": "vie $0", + "magic": "magie $0", + "rupees": "rubis $0", + "0": "Bâton Mojo $0", + "1": "Noix Mojo $0", + "2": "Bombes $0", + "3": "Arc des Fées $0", + "4": "Flèche de Feu", + "5": "Feu de Din", + "6": "Lance-Pierre des Fées $0", + "7": "Ocarina des Fées", + "8": "Ocarina of Temps", + "9": "Missiles Teigneux $0", + "10": "Grappin", + "11": "Super Grappin", + "12": "Flèche de Glace", + "13": "Vent de Farore", + "14": "Boomerang", + "15": "Monocle de Vérité", + "16": "Haricot Magique $0", + "17": "Masse des Titans", + "18": "Flèche de Lumière", + "19": "Amour de Nayru", + "20": "Bouteille Vide", + "21": "Potion Rouge", + "22": "Potion Verte", + "23": "Potion Bleue", + "24": "Fée", + "25": "Poisson", + "26": "Lait de Lon Lon", + "27": "Lettre de Ruto", + "28": "Flammme Bleue", + "29": "Insectes", + "30": "Âme", + "31": "Lait de Lon Lon (moitié)", + "32": "Esprit", + "33": "Oeuf Curieux", + "34": "Poulet", + "35": "Lettre de Zelda", + "36": "Masque du Renard", + "37": "Masque de Mort", + "38": "Masque d'Effroi", + "39": "Masque du Lapin", + "40": "Masque de Goron", + "41": "Masque de Zora", + "42": "Masque de Gerudo", + "43": "Masque de Vérité", + "44": "VENDU", + "45": "Oeuf de Poche", + "46": "Cocotte de poche", + "47": "P'tit Poulet", + "48": "Champignon suspect", + "49": "Mixture suspecte", + "50": "Scie du chasseur", + "51": "Épée de Goron (brisée)", + "52": "Ordonnance", + "53": "Crapaud-qui-louche", + "54": "Gouttes", + "55": "Certificat", + "56": "Arc et Flèche de Feu", + "57": "Arc et Flèche de Glace", + "58": "Arc et Flèche de Lumière", + "59": "Épée Kokiri", + "60": "Épée de Légende", + "61": "Lame des Géants", + "62": "Bouclier Mojo", + "63": "Bouclier Hylien", + "64": "Bouclier Miroir", + "65": "Tunique Kokiri", + "66": "Tunique Goron", + "67": "Tunique Zora", + "68": "Bottes Kokiri", + "69": "Bottes de plomb", + "70": "Bottes des airs", + "71": "Sac de graines (Contient 30)", + "72": "Sac de graines (Contient 40)", + "73": "Sac de graines (Contient 50)", + "74": "Carquois (Contient 30)", + "75": "Carquois (Contient 40)", + "76": "Carquois (Contient 50)", + "77": "Sac de bombes (Contient 20)", + "78": "Sac de bombes (Contient 30)", + "79": "Sac de bombes (Contient 40)", + "80": "Bracelet Goron", + "81": "Gantelets d'argent", + "82": "Gentelets d'or", + "83": "Écaille d'argent", + "84": "Écaille d'or", + "85": "Lame des Géants (Brisée)", + "86": "GRANDE BOURSE", + "87": "Bourse de Géant", + "88": "Deku Seeds", + "89": "Canne à pèche", + "90": "Menuet des Bois", + "91": "Boléro du Feu", + "92": "Sérénade de l'Eau", + "93": "Requiem des Esprits", + "94": "Nocturne de l'Ombre", + "95": "Prélude de la Lumière", + "96": "Berceuse de Zelda", + "97": "Chant d'Epona", + "98": "Chant de Saria", + "99": "Chant du Soleil", + "100": "Chant du Temps", + "101": "Chant des Tempêtes", + "102": "Médaillon de la Forêt", + "103": "Médaillon du Feu", + "104": "Médaillon de l'Eau", + "105": "Médaillon de l'Esprit", + "106": "Médaillon de l'Ombre", + "107": "Médaillon de la Lumière", + "108": "Émeraude Kokiri", + "109": "Rubis Goron", + "110": "Saphir Zora", + "111": "Pierre de Souffrance", + "112": "Carte Gerudo", + "113": "Skulltula d'or $0", + "114": "Coeur d'Énergie $0", + "115": "Quart de Coeur", + "116": "Clé d'or", + "117": "Boussole", + "118": "Carte du Donjon", + "119": "Petite Clé", + "120": "PETITE BOUTEILLE DE MAGIE", + "121": "GRANDE BOUTEILLE DE MAGIE", + "122": "QUART DE COEUR 2", + "123": "INVALIDE 1", + "124": "INVALIDE 2", + "125": "INVALIDE 3", + "126": "INVALIDE 4", + "127": "INVALIDE 5", + "128": "INVALIDE 6", + "129": "INVALIDE 7", + "130": "Lait de Lon Lon", + "131": "Coeur de Vie", + "132": "Rubis Vert", + "133": "Rubis Bleu", + "134": "Rubis Rouge", + "135": "Rubis Pourpre", + "136": "Énorme Rubis", + "137": "INVALIDE 8", + "138": "BÂTON MOJO 5", + "139": "BÂTON MOJO 10", + "140": "NOIX MOJO 5", + "141": "NOIX MOJO 10", + "142": "BOMBES 5", + "143": "BOMBES 10", + "144": "BOMBES 20", + "145": "BOMBES 30", + "146": "ARROWS SMALL", + "147": "ARROWS MEDIUM", + "148": "ARROWS LARGE", + "149": "GRAINES MOJO 30", + "150": "MISSILES TEIGNEUX 5", + "151": "MISSILES TEIGNEUX 20", + "152": "AMÉLIORATION BÂTON MOJO 20", + "153": "AMÉLIORATION BÂTON MOJO 30", + "154": "AMÉLIORATION NOIX MOJO 30", + "155": "AMÉLIORATION NOIX MOJO 40", + "256": "Désert Hanté", + "257": "Forteresse Gerudo", + "258": "Vallée Gerudo", + "259": "Laboratoire du Lac", + "260": "Ranch Lon Lon", + "261": "Place du Marché", + "262": "Plaine d'Hyrule", + "263": "Montagne du Péril", + "264": "Village Cocorico", + "265": "Bois Perdus", + "266": "Forêt Kokiri", + "267": "Domaine Zora", + "268": "", + "269": "", + "270": "", + "271": "", + "272": "", + "273": "", + "274": "", + "275": "", + "276": "", + "277": "", + "278": "", + "279": "", + "280": "", + "281": "", + "282": "", + "283": "", + "284": "", + "285": "", + "286": "", + "287": "", + "288": "", + "289": "", + "290": "", + "291": "", + "292": "Plaine d'Hyrule", + "293": "Village Cocorico", + "294": "Cimetière", + "295": "Rivière Zora", + "296": "Forêt Kokiri", + "297": "Bosquet Sacré", + "298": "Lac Hylia", + "299": "Domaine Zora", + "300": "Fountaine Zora", + "301": "Vallée Gerudo", + "302": "Bois Perdus", + "303": "Colosse du Désert", + "304": "Forteresse Gerudo", + "305": "Désert Hanté", + "306": "Place du Marché", + "307": "Château d'Hyrule", + "308": "Chemin du Péril", + "309": "Cratère du Péril", + "310": "Village Goron", + "311": "Ranch Lon Lon", + "312": "Point d'interrogation", + "313": "Château de Ganon" +} \ No newline at end of file diff --git a/OTRExporter/assets/accessibility/texts/kaleidoscope_ger.json b/OTRExporter/assets/accessibility/texts/kaleidoscope_ger.json new file mode 100644 index 000000000..0591b8d4b --- /dev/null +++ b/OTRExporter/assets/accessibility/texts/kaleidoscope_ger.json @@ -0,0 +1,219 @@ +{ + "health": "Energie $0", + "magic": "Magie $0", + "rupees": "Rubine $0", + "0": "Deku-Stab $0", + "1": "Deku-Nuß $0", + "2": "Bombe $0", + "3": "Feen-Bogen $0", + "4": "Feuer-Pfeil", + "5": "Dins Feuerinferno", + "6": "Feen-Schleuder $0", + "7": "Feen-Okarina", + "8": "Okarina der Zeit", + "9": "Krabbelmine $0", + "10": "Fanghaken", + "11": "Enterhaken", + "12": "Eis-Pfeil", + "13": "Farores Donnersturm", + "14": "Bumerang", + "15": "Auge der Wahrheit", + "16": "Wundererbsen $0", + "17": "Stahlhammer", + "18": "Licht-Pfeil", + "19": "Nayrus Umarmung", + "20": "Flasche", + "21": "Rotes Elixier", + "22": "Grünes Elixier", + "23": "Blaues Elixier", + "24": "Fee", + "25": "Fisch", + "26": "Milch", + "27": "Brief", + "28": "Blaues Feuer", + "29": "Käfer", + "30": "Nachtschwärmer", + "31": "Milch (1/2)", + "32": "Irrlicht", + "33": "Seltsames Ei", + "34": "Huhn", + "35": "Zeldas Brief", + "36": "Fuchs-Maske", + "37": "Schädel-Maske", + "38": "Geister-Maske", + "39": "Hasenohren", + "40": "Goronen-Maske", + "41": "Zora-Maske", + "42": "Gerudo-Maske", + "43": "Maske des Wissens", + "44": "Verkauft", + "45": "Ei", + "46": "Kiki", + "47": "Henni", + "48": "Schimmelpilz", + "49": "Modertrank", + "50": "Säge", + "51": "Goronen-Schwert (zerbrochen)", + "52": "Rezept", + "53": "Glotzfrosch", + "54": "Augentropfen", + "55": "Zertifikat", + "56": "Bogen Feuer-Pfeil", + "57": "Bogen Eis-Pfeil", + "58": "Bogen Licht-Pfeil", + "59": "Kokiri-Schwert", + "60": "Master-Schwert", + "61": "Langschwert", + "62": "Deku-schild", + "63": "Hylia-Schild", + "64": "Spiegel-Schild", + "65": "Kokiri-Rüstung", + "66": "Goronen-Rüstung", + "67": "Zora-Rüstung", + "68": "Lederstiefel", + "69": "Eisenstiefel", + "70": "Gleitstiefel", + "71": "Munitionstasche (30)", + "72": "Munitionstasche (40)", + "73": "Munitionstasche (50)", + "74": "Köcher (30)", + "75": "Köcher (40)", + "76": "Köcher (50)", + "77": "Bombentasche (20)", + "78": "Bombentasche (30)", + "79": "Bombentasche (40)", + "80": "Goronen-Armband", + "81": "Krafthandschuh", + "82": "Titanhandschuh", + "83": "Silberschuppe", + "84": "Goldschuppe", + "85": "Langschwert (gebrochen)", + "86": "Große Börse", + "87": "Riesenbörse", + "88": "Deku-Kerne", + "89": "Angel", + "90": "Menuett des Waldes", + "91": "Bolero des Feuers", + "92": "Serenade des Wassers", + "93": "Requiem der Geister", + "94": "Nocturne des Schattens", + "95": "Kantate des Lichts", + "96": "Zeldas Wiegenlied", + "97": "Eponas Lied", + "98": "Salias Lied", + "99": "Hymne der Sonne", + "100": "Hymne der Zeit", + "101": "Song of Storms", + "102": "Amulett des Waldes", + "103": "Amulett des Feuers", + "104": "Amulett des Wassers", + "105": "Amulett der Geister", + "106": "Amulett des Schattens", + "107": "Amulett des Lichts", + "108": "Kokiri-Smaragd", + "109": "Goronen-Opal", + "110": "Zora-Saphir", + "111": "Stein des Wissens", + "112": "Gerudo-Paß", + "113": "Skulltula-Symbol $0", + "114": "Herzcontainer $0", + "115": "Herzteil", + "116": "Master-Schlüssel", + "117": "Kompaß", + "118": "Labyrinth-Karte", + "119": "Kleiner Schlüssel", + "120": "MAGIE KLEIN", + "121": "MAGIE GROß", + "122": "HERZTEIL 2", + "123": "UNGÜLTIG 1", + "124": "UNGÜLTIG 2", + "125": "UNGÜLTIG 3", + "126": "UNGÜLTIG 4", + "127": "UNGÜLTIG 5", + "128": "UNGÜLTIG 6", + "129": "UNGÜLTIG 7", + "130": "Milch", + "131": "Herz", + "132": "ein Rubin", + "133": "5 Rubine", + "134": "20 Rubine", + "135": "50 Rubine", + "136": "200 Rubine", + "137": "UNGÜLTIG 8", + "138": "STÄBE 5", + "139": "STÄBE 10", + "140": "NÜSSE 5", + "141": "NÜSSE 10", + "142": "BOMBEN 5", + "143": "BOMBEN 10", + "144": "BOMBEN 20", + "145": "BOMBEN 30", + "146": "PFEILE KLEIN", + "147": "PFEILE MITTEL", + "148": "PFEILE GROß", + "149": "KERNE 30", + "150": "KRABBELMINEN 5", + "151": "KRABBELMINEN 20", + "152": "STAB UPGRADE 20", + "153": "STAB UPGRADE 30", + "154": "NUß UPGRADE 30", + "155": "NUß UPGRADE 40", + "256": "Gespensterwüste", + "257": "Gerudo-Festung", + "258": "Gerudotal", + "259": "Hylia-See", + "260": "Lon Lon-Farm", + "261": "Marktplatz", + "262": "Hylianische Steppe", + "263": "Todesberg", + "264": "Kakariko", + "265": "Verlorene Wälder", + "266": "Kokiri-Wald", + "267": "Zoras Reich", + "268": "", + "269": "", + "270": "", + "271": "", + "272": "", + "273": "", + "274": "", + "275": "", + "276": "", + "277": "", + "278": "", + "279": "", + "280": "", + "281": "", + "282": "", + "283": "", + "284": "", + "285": "", + "286": "", + "287": "", + "288": "", + "289": "", + "290": "", + "291": "", + "292": "Hylianische Steppe", + "293": "Kakariko", + "294": "Friedhof", + "295": "Zora-Fluss", + "296": "Kokiri-Wald", + "297": "Heilige Lichtung", + "298": "Hylia-See", + "299": "Zoras Reich", + "300": "Zoras Quelle", + "301": "Gerudotal", + "302": "Verlorene Wälder", + "303": "Wüstenkoloss", + "304": "Gerudo-Festung", + "305": "Gespensterwüste", + "306": "Marktplatz", + "307": "Schloß Hyrule", + "308": "Pfad zum Todesberg", + "309": "Todeskrater", + "310": "Goronia", + "311": "Lon Lon-Farm", + "312": "Fragezeichen", + "313": "Teufelsturm" +} \ No newline at end of file diff --git a/OTRExporter/assets/accessibility/texts/misc_eng.json b/OTRExporter/assets/accessibility/texts/misc_eng.json new file mode 100644 index 000000000..db59f3deb --- /dev/null +++ b/OTRExporter/assets/accessibility/texts/misc_eng.json @@ -0,0 +1,18 @@ +{ + "minutes_plural" : "$0 minutes", + "minutes_singular" : "$0 minute", + "seconds_plural" : "$0 seconds", + "seconds_singular" : "$0 second", + "input_button_a": "the A button", + "input_button_b": "the B button", + "input_button_c": "the C button", + "input_button_l": "the L button", + "input_button_r": "the R button", + "input_button_z": "the Z button", + "input_button_c_up": "C Up", + "input_button_c_down": "C Down", + "input_button_c_left": "C Left", + "input_button_c_right": "C Right", + "input_analog_stick": "the Analog Stick", + "input_d_pad": "the D-Pad" +} \ No newline at end of file diff --git a/OTRExporter/assets/accessibility/texts/misc_fra.json b/OTRExporter/assets/accessibility/texts/misc_fra.json new file mode 100644 index 000000000..e37268b95 --- /dev/null +++ b/OTRExporter/assets/accessibility/texts/misc_fra.json @@ -0,0 +1,18 @@ +{ + "minutes_plural" : "$0 minutes", + "minutes_singular" : "$0 minute", + "seconds_plural" : "$0 secondes", + "seconds_singular" : "$0 seconde", + "input_button_a": "le bouton A", + "input_button_b": "le bouton B", + "input_button_c": "le bouton C", + "input_button_l": "le bouton L", + "input_button_r": "le bouton R", + "input_button_z": "le bouton Z", + "input_button_c_up": "C Haut", + "input_button_c_down": "C Bas", + "input_button_c_left": "C Gauche", + "input_button_c_right": "C Droit", + "input_analog_stick": "le Stick Analogique", + "input_d_pad": "D-Pad" +} \ No newline at end of file diff --git a/OTRExporter/assets/accessibility/texts/misc_ger.json b/OTRExporter/assets/accessibility/texts/misc_ger.json new file mode 100644 index 000000000..23b887861 --- /dev/null +++ b/OTRExporter/assets/accessibility/texts/misc_ger.json @@ -0,0 +1,18 @@ +{ + "minutes_plural" : "$0 Minuten", + "minutes_singular" : "eine Minute", + "seconds_plural" : "$0 Sekunden", + "seconds_singular" : "eine Sekunde", + "input_button_a": "den A-Knopf", + "input_button_b": "den B-Knopf", + "input_button_c": "den C-Knopf", + "input_button_l": "den L-Knopf", + "input_button_r": "den R-Knopf", + "input_button_z": "den Z-Knopf", + "input_button_c_up": "C Oben", + "input_button_c_down": "C Unten", + "input_button_c_left": "C Links", + "input_button_c_right": "C Rechts", + "input_analog_stick": "den Analog-Stick", + "input_d_pad": "das Steuerkreuz" +} \ No newline at end of file diff --git a/OTRExporter/assets/accessibility/texts/scenes_eng.json b/OTRExporter/assets/accessibility/texts/scenes_eng.json new file mode 100644 index 000000000..7f9397bbf --- /dev/null +++ b/OTRExporter/assets/accessibility/texts/scenes_eng.json @@ -0,0 +1,112 @@ +{ + "0": "Inside the Deku Tree", + "1": "Dodongo's Cavern", + "2": "Inside Jabu-Jabu's Belly", + "3": "Forest Temple", + "4": "Fire Temple", + "5": "Water Temple", + "6": "Spirit Temple", + "7": "Shadow Temple", + "8": "Bottom of The Well", + "9": "Ice Cavern", + "10": "", // Stairs to Ganondorf's Lair (No title card) + "11": "Gerudo Training Ground", + "12": "Thieves' Hideout", + "13": "Ganon's Castle", + "14": "", // Escape from Ganon's Castle (No title card) + "15": "", // Escape from Ganon's Castle 5 (No title card)x + "16": "Treasure Box Shop", + "17": "Parasitic Armored Arachnid - Gohma", + "18": "Infernal Dinosaur - King Dodongo", + "19": "Bio-electric Anemone - Barinade", + "20": "Evil Spirit from Beyond - Phantom Ganon", + "21": "Subterranean Lava Dragon - Volvagia", + "22": "Giant Aquatic Amoeba - Morpha", + "23": "Sorceress Sisters - Twinrova", + "24": "Phantom Shadow Beast - Bongo Bongo", + "25": "Great King of Evil - Ganondorf", + "26": "", + "27": "", // Entrance to Market (No title card) + "28": "", + "29": "", + "30": "Back Alley", + "31": "Back Alley", + "32": "Market", + "33": "Market", + "34": "Market", + "35": "", // Temple of Time Exterior (No title card) + "36": "SCENE_SHRINE_N", + "37": "SCENE_SHRINE_R", + "38": "", // House of the Know-it-All Brothers (No title card) + "39": "", // House of Twins (No title card) + "40": "", // House of the Great Mido (No title card) + "41": "", // Saria's House (No title card) + "42": "", // Kakariko House 1 (No title card) + "43": "", // Back Alley House 1 (No title card) + "44": "Bazaar", + "45": "Kokiri Shop", + "46": "Goron Shop", + "47": "Zora Shop", + "48": "", // Closed Shop (No title card) + "49": "Potion Shop", + "50": "", // Bombchu Shop (No title card) + "51": "Happy Mask Shop", + "52": "", // Link's House (No title card) + "53": "", // Dog Lady's House (No title card) + "54": "Stable", + "55": "", // Impa's House (No title card) + "56": "Lakeside Laboratory", + "57": "", // Running Man's Tent (No title card) + "58": "Gravekeepers Hut", + "59": "Great Fairy's Fountain", + "60": "Fairy's Fountain", + "61": "Great Fairy's Fountain", + "62": "", // Grottos (No title card) + "63": "", // Tomb 1 (No title card) + "64": "", // Tomb 2 (No title card) + "65": "Royal Family's Tomb", + "66": "Shooting Gallery", + "67": "Temple of Time", + "68": "Chamber of The Sages", + "69": "Castle Courtyard", + "70": "Castle Courtyard", + "71": "", // Goddesses Cutscene (No title card) + "72": "Unknown Place", + "73": "Fishing Pond", + "74": "Castle Courtyard", + "75": "Bombchu Bowling Alley", + "76": "", // Lon Lon Ranch House/Silo (No title card) + "77": "", // Guard House (No title card) + "78": "", // Potion Shop (No title card) + "79": "Ganon", + "80": "House of Skulltula", + "81": "Hyrule Field", + "82": "Kakariko Village", + "83": "Graveyard", + "84": "Zora's River", + "85": "Kokiri Forest", + "86": "Sacred Forest Meadow", + "87": "Lake Hylia", + "88": "Zoras Domain", + "89": "Zoras Fountain", + "90": "Gerudo Valley", + "91": "Lost Woods", + "92": "Desert Colossus", + "93": "Gerudo's Fortress", + "94": "Haunted Wasteland", + "95": "Hyrule Castle", + "96": "Death Mountain Trail", + "97": "Death Mountain Crater", + "98": "Goron City", + "99": "Lon Lon Ranch", + "100": "", + "101": "", // Debug: Test Map (No title card) + "102": "", // Debug: Test Room (No title card) + "103": "", // Debug: Depth Test (No title card) + "104": "", // Debug: Stalfos Miniboss Room (No title card) + "105": "", // Debug: Stalfos Boss Room (No title card) + "106": "", // Debug: Dark Link Room (No title card) + "107": "", + "108": "", // Debug: SRD Room (No title card) + "109": "" // Debug: Treasure Chest Warp (No title card) +} \ No newline at end of file diff --git a/OTRExporter/assets/accessibility/texts/scenes_fra.json b/OTRExporter/assets/accessibility/texts/scenes_fra.json new file mode 100644 index 000000000..fa36c8840 --- /dev/null +++ b/OTRExporter/assets/accessibility/texts/scenes_fra.json @@ -0,0 +1,112 @@ +{ + "0": "Abre Mojo", + "1": "Caverne Dodongo", + "2": "Ventre de Jabu-Jabu", + "3": "Temple de la Forêt", + "4": "Temple du Feu", + "5": "Temple de l'Eau", + "6": "Temple de l'Esprit", + "7": "Temple de l'Ombre", + "8": "Puits", + "9": "Caverne Polaire", + "10": "", // Escaliers vers le Repaire de Ganondorf (No title card) + "11": "Gymnase Gerudo", + "12": "Repaire des Voleurs", + "13": "Tour de Ganon", + "14": "", // Fuite du Château de Ganon (No title card) + "15": "", // Fuite du Château de Ganon 5 (No title card) + "16": "Chasse aux Trésors", + "17": "Monstre Insectoide Géant - Gohma", + "18": "Dinosaure Infernal - King Dodongo", + "19": "Anémone Bio-Electrique - Barinade", + "20": "Esprit Maléfique de l'Au-Delà - Ganon Spectral", + "21": "Dragon des Profondeurs - Volcania", + "22": "Amibe Aquatique Géante - Morpha", + "23": "Sorcières Jumelles - Duo Maléfique", + "24": "Monstre de l'Ombre - Bongo Bongo", + "25": "Seigneur du Malin - Ganondorf", + "26": "", + "27": "", // Entrée vers le Marché (No title card) + "28": "", + "29": "", + "30": "Ruelle", + "31": "Ruelle", + "32": "Place du Marché", + "33": "Place du Marché", + "34": "Place du Marché", + "35": "", // Extérieur du Temple du Temps (No title card) + "36": "SCENE_SHRINE_N", + "37": "SCENE_SHRINE_R", + "38": "", // Cabane des Frères Je-Sais-Tout (No title card) + "39": "", // Cabane des Jumelles (No title card) + "40": "", // Cabane du Grand Mido (No title card) + "41": "", // Cabane de Saria (No title card) + "42": "", // Maison du Village Cocorico 1 (No title card) + "43": "", // Maison de la Ruelle 1 (No title card) + "44": "Bazar", + "45": "Boutique Kokiri", + "46": "Boutique Goron", + "47": "Boutique Zora", + "48": "", // Magasin Fermé (No title card) + "49": "Apothicaire", + "50": "", // Magasin de Missiles (No title card) + "51": "Foire aux Masques", + "52": "", // Cabane de Link (No title card) + "53": "", // Dog Lady's House (No title card) + "54": "Étable", + "55": "", // Maison d'Impa (No title card) + "56": "Laboratoire du Lac", + "57": "", // Tente du Marathonien (No title card) + "58": "Cabane du fossoyeur", + "59": "Fountaine Royale des Fées", + "60": "Fountaine des Fées", + "61": "Fountaine Royale des Fées", + "62": "", // Grottes (No title card) + "63": "", // Tombe 1 (No title card) + "64": "", // Tombe 2 (No title card) + "65": "Tombe Royale", + "66": "Jeu d'adresse", + "67": "Temple du Temps", + "68": "Sanctuaire des Sages", + "69": "Cour du Château", + "70": "Cour du Château", + "71": "", // Goddesses Cutscene (No title card) + "72": "Endroit Inconnu", + "73": "Étang", + "74": "Cour du Château", + "75": "Bowling Teigneux", + "76": "", // Lon Lon Ranch House/Silo (No title card) + "77": "", // Guard House (No title card) + "78": "", // Potion Shop (No title card) + "79": "Ganon", + "80": "Maison des Araignées", + "81": "Plaine d'Hyrule", + "82": "Village Cocorico", + "83": "Cimetière", + "84": "Fleuve Zora", + "85": "Forêt Kokiri", + "86": "Bosquet Sacré", + "87": "Lac Hylia", + "88": "Domaine Zora", + "89": "Fontaine Zora", + "90": "Vallée Gerudo", + "91": "Bois Perdu", + "92": "Colosse du Désert", + "93": "Forteresse Gerudo", + "94": "Désert Hanté", + "95": "Château d'Hyrule", + "96": "Chemin du Péril", + "97": "Cratère du Péril", + "98": "Village Goron", + "99": "Ranch Lon Lon", + "100": "", + "101": "", // Debug: Test Map (No title card) + "102": "", // Debug: Test Room (No title card) + "103": "", // Debug: Depth Test (No title card) + "104": "", // Debug: Stalfos Miniboss Room (No title card) + "105": "", // Debug: Stalfos Boss Room (No title card) + "106": "", // Debug: Dark Link Room (No title card) + "107": "", + "108": "", // Debug: SRD Room (No title card) + "109": "" // Debug: Treasure Chest Warp (No title card) +} \ No newline at end of file diff --git a/OTRExporter/assets/accessibility/texts/scenes_ger.json b/OTRExporter/assets/accessibility/texts/scenes_ger.json new file mode 100644 index 000000000..a3f7370e0 --- /dev/null +++ b/OTRExporter/assets/accessibility/texts/scenes_ger.json @@ -0,0 +1,112 @@ +{ + "0": "Im Deku-Baum", + "1": "Dodongos Höhle", + "2": "Jabu-Jabus Bauch", + "3": "Waldtempel", + "4": "Feuertempel", + "5": "Wassertempel", + "6": "Geistertempel", + "7": "Schattentempel", + "8": "Grund des Brunnens", + "9": "Eishöhle", + "10": "", // Treppe zu Ganondorfs Verließ (Keine Title-Card) + "11": "Gerudo-Arena", + "12": "Diebesversteck", + "13": "Ganons Schloß", + "14": "", // Flucht aus Ganons Schloß (Keine Title-Card) + "15": "", // Flucht aus Ganons Schloß 5 (Keine Title-Card) + "16": "Truhenlotterie", + "17": "Gepanzerter Spinnenparasit - Gohma", + "18": "Infernosaurus - King Dodongo", + "19": "Elektroterristrisches Biotentakel - Barinade", + "20": "Reitendes Unheil - Phantom-Ganon", + "21": "Subterraner Lavadrachoid - Volvagia", + "22": "Aquamöbes Wassertentakel - Morpha", + "23": "Höllische Hexenarmada - Killa Ohmaz", + "24": "Bestialische Schattenmonstrosität - Bongo Bongo", + "25": "Großmeister des Bösen - Ganondorf", + "26": "", + "27": "", // Eingang zum Marktplatz (Keine Title-Card) + "28": "", + "29": "", + "30": "Seitenstraße", + "31": "Seitenstraße", + "32": "Marktplatz", + "33": "Marktplatz", + "34": "Marktplatz", + "35": "", // Vor der Zitadelle der Zeit (Keine Title-Card) + "36": "SCENE_SHRINE_N", + "37": "SCENE_SHRINE_R", + "38": "", // Haus der Allwissenden Brüder (Keine Title-Card) + "39": "", // Haus der Zwillinge (Keine Title-Card) + "40": "", // Midos Haus (Keine Title-Card) + "41": "", // Salias Haus (Keine Title-Card) + "42": "", // Kakariko Haus 1 (Keine Title-Card) + "43": "", // Steinstraßen Haus 1 (Keine Title-Card) + "44": "Basar", + "45": "Kokiri-Laden", + "46": "Goronen-Laden", + "47": "Zora-Laden", + "48": "", // Geschlossener Laden (Keine Title-Card) + "49": "Magie-Laden", + "50": "", // Krabbelminen-Laden (Keine Title-Card) + "51": "Maskenhändler", + "52": "", // Links Haus (Keine Title-Card) + "53": "", // Haus der Hunde-Dame (Keine Title-Card) + "54": "Stall", + "55": "", // Impas Haus (Keine Title-Card) + "56": "Hylia-See Laboratorium", + "57": "", // Zelt des Rennläufers (Keine Title-Card) + "58": "Hütte des Totengräbers", + "59": "Feen-Quelle", + "60": "Feen-Brunnen", + "61": "Feen-Quelle", + "62": "", // Grotten (Keine Title-Card) + "63": "", // Grab 1 (Keine Title-Card) + "64": "", // Grab 2 (Keine Title-Card) + "65": "Königsgrab", + "66": "Schießbude", + "67": "Zitadelle der Zeit", + "68": "Halle der Weisen", + "69": "Burghof", + "70": "Burghof", + "71": "", // Göttinnen Cutscene (Keine Title-Card) + "72": "Unbekannter Ort", + "73": "Fischweiher", + "74": "Burghof", + "75": "Minenbowlingbahn", + "76": "", // Lon Lon-Farm Haus/Silo (Keine Title-Card) + "77": "", // Wachposten (Keine Title-Card) + "78": "", // Magie-Laden (Keine Title-Card) + "79": "Ganon", + "80": "Skulltulas Haus", + "81": "Hylianische Steppe", + "82": "Kakariko", + "83": "Friedhof", + "84": "Zora-Fluß", + "85": "Kokiri-Wald", + "86": "Waldlichtung", + "87": "Hylia-See", + "88": "Zoras Reich", + "89": "Zoras Quelle", + "90": "Gerudotal", + "91": "Verlorene Wälder", + "92": "Wüstenkoloss", + "93": "Gerudo-Festung", + "94": "Geisterwüste", + "95": "Schloß Hyrule", + "96": "Pfad zum Todesberg", + "97": "Todeskrater", + "98": "Goronia", + "99": "Lon Lon-Farm", + "100": "", + "101": "", // Debug: Test Karte (Keine Title-Card) + "102": "", // Debug: Test Raum (Keine Title-Card) + "103": "", // Debug: Tiefen Test (Keine Title-Card) + "104": "", // Debug: Stalfos-Ritter Miniboss Raum (Keine Title-Card) + "105": "", // Debug: Stalfos-Ritter Boss Raum (Keine Title-Card) + "106": "", // Debug: Schwarzer Link Raum (Keine Title-Card) + "107": "", + "108": "", // Debug: SRD Raum (Keine Title-Card) + "109": "" // Debug: Schatzkisten Teleport (Keine Title-Card) +} \ No newline at end of file diff --git a/soh/CMakeLists.txt b/soh/CMakeLists.txt index 3eeaa645c..8475ebe0e 100644 --- a/soh/CMakeLists.txt +++ b/soh/CMakeLists.txt @@ -5,6 +5,12 @@ set(CMAKE_SYSTEM_VERSION 10.0 CACHE STRING "" FORCE) project(soh LANGUAGES C CXX) set(CMAKE_CXX_STANDARD 20 CACHE STRING "The C++ standard to use") +if (CMAKE_SYSTEM_NAME STREQUAL "Darwin") + enable_language(OBJCXX) + set(CMAKE_OBJC_FLAGS "${CMAKE_OBJC_FLAGS} -fobjc-arc") + set(CMAKE_OBJCXX_FLAGS "${CMAKE_OBJCXX_FLAGS} -fobjc-arc") +endif() + set (BUILD_UTILS OFF CACHE STRING "no utilities") set (BUILD_SHARED_LIBS OFF CACHE STRING "install/link shared instead of static libs") @@ -116,6 +122,10 @@ source_group("include" FILES ${Header_Files__include}) file(GLOB soh__ RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "soh/*.c" "soh/*.cpp" "soh/*.h") source_group("soh" FILES ${soh__}) +if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + set_source_files_properties(soh/OTRGlobals.cpp PROPERTIES COMPILE_FLAGS "/utf-8") +endif() + # soh/enhancements {{{ file(GLOB_RECURSE soh__Enhancements RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "soh/Enhancements/*.c" @@ -123,13 +133,25 @@ file(GLOB_RECURSE soh__Enhancements RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "soh/Enhancements/*.h" "soh/Enhancements/*.hpp" "soh/Enhancements/*_extern.inc" + "soh/Enhancements/*.mm" ) list(REMOVE_ITEM soh__Enhancements "soh/Enhancements/gamecommand.h") list(FILTER soh__Enhancements EXCLUDE REGEX "soh/Enhancements/gfx.*") + +# handle crowd control removals list(REMOVE_ITEM soh__Enhancements "soh/Enhancements/crowd-control/soh.cs") if (!BUILD_CROWD_CONTROL) - list(FILTER soh__Enhancements EXCLUDE REGEX "soh/Enhancements/crowd-control/.*") + list(FILTER soh__Enhancements EXCLUDE REGEX "soh/Enhancements/crowd-control/*") +endif() + +# handle speechsynthesizer removals +if (CMAKE_SYSTEM_NAME STREQUAL "Windows") + list(FILTER soh__Enhancements EXCLUDE REGEX "soh/Enhancements/speechsynthesizer/Darwin*") +elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin") + list(FILTER soh__Enhancements EXCLUDE REGEX "soh/Enhancements/speechsynthesizer/SAPI*") +else() + list(FILTER soh__Enhancements EXCLUDE REGEX "soh/Enhancements/speechsynthesizer/(Darwin|SAPI).*") endif() source_group("soh\\Enhancements" REGULAR_EXPRESSION "soh/Enhancements/*") @@ -145,6 +167,12 @@ source_group("soh\\Enhancements\\randomizer" REGULAR_EXPRESSION "soh/Enhancement source_group("soh\\Enhancements\\randomizer\\3drando" REGULAR_EXPRESSION "soh/Enhancements/randomizer/3drando/*") source_group("soh\\Enhancements\\randomizer\\3drando\\hint_list" REGULAR_EXPRESSION "soh/Enhancements/randomizer/3drando/hint_list/*") source_group("soh\\Enhancements\\randomizer\\3drando\\location_access" REGULAR_EXPRESSION "soh/Enhancements/randomizer/3drando/location_access/*") +source_group("soh\\Enhancements\\speechsynthesizer" REGULAR_EXPRESSION "soh/Enhancements/speechsynthesizer/*") +source_group("soh\\Enhancements\\tts" REGULAR_EXPRESSION "soh/Enhancements/tts/*") + +if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + set_source_files_properties(soh/Enhancements/tts/tts.cpp PROPERTIES COMPILE_FLAGS "/utf-8") +endif() # }}} # soh/resource {{{ @@ -225,7 +253,8 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Windows") endif() elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin") set_target_properties(${PROJECT_NAME} PROPERTIES - OUTPUT_NAME "soh-macos" + XCODE_ATTRIBUTE_CLANG_ENABLE_OBJC_ARC YES + OUTPUT_NAME "soh-macos" ) elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") set_target_properties(${PROJECT_NAME} PROPERTIES diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor.h b/soh/soh/Enhancements/game-interactor/GameInteractor.h index a3e2b4c5c..81a42d702 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor.h +++ b/soh/soh/Enhancements/game-interactor/GameInteractor.h @@ -4,7 +4,6 @@ #define GameInteractor_h #include "GameInteractionEffect.h" -#include "z64.h" typedef enum { /* 0x00 */ GI_LINK_SIZE_NORMAL, @@ -87,13 +86,29 @@ public: DEFINE_HOOK(OnLoadGame, void(int32_t fileNum)); DEFINE_HOOK(OnExitGame, void(int32_t fileNum)); DEFINE_HOOK(OnGameFrameUpdate, void()); - DEFINE_HOOK(OnReceiveItem, void(u8 item)); - DEFINE_HOOK(OnSceneInit, void(s16 sceneNum)); + DEFINE_HOOK(OnReceiveItem, void(uint8_t item)); + DEFINE_HOOK(OnSceneInit, void(int16_t sceneNum)); DEFINE_HOOK(OnPlayerUpdate, void()); DEFINE_HOOK(OnSaveFile, void(int32_t fileNum)); DEFINE_HOOK(OnLoadFile, void(int32_t fileNum)); DEFINE_HOOK(OnDeleteFile, void(int32_t fileNum)); + + DEFINE_HOOK(OnDialogMessage, void()); + DEFINE_HOOK(OnPresentTitleCard, void()); + DEFINE_HOOK(OnInterfaceUpdate, void()); + DEFINE_HOOK(OnKaleidoscopeUpdate, void(int16_t inDungeonScene)); + + DEFINE_HOOK(OnPresentFileSelect, void()); + DEFINE_HOOK(OnUpdateFileSelectSelection, void(uint16_t optionIndex)); + DEFINE_HOOK(OnUpdateFileCopySelection, void(uint16_t optionIndex)); + DEFINE_HOOK(OnUpdateFileCopyConfirmationSelection, void(uint16_t optionIndex)); + DEFINE_HOOK(OnUpdateFileEraseSelection, void(uint16_t optionIndex)); + DEFINE_HOOK(OnUpdateFileEraseConfirmationSelection, void(uint16_t optionIndex)); + DEFINE_HOOK(OnUpdateFileAudioSelection, void(uint8_t optionIndex)); + DEFINE_HOOK(OnUpdateFileTargetSelection, void(uint8_t optionIndex)); + + DEFINE_HOOK(OnSetGameLanguage, void()); // Helpers static bool IsSaveLoaded(); diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp index 2ed4758e9..4d65a0435 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp @@ -1,9 +1,5 @@ #include "GameInteractor_Hooks.h" -extern "C" { -extern PlayState* gPlayState; -} - // MARK: - Gameplay void GameInteractor_ExecuteOnLoadGame(int32_t fileNum) { @@ -18,11 +14,11 @@ void GameInteractor_ExecuteOnGameFrameUpdate() { GameInteractor::Instance->ExecuteHooks(); } -void GameInteractor_ExecuteOnReceiveItemHooks(u8 item) { +void GameInteractor_ExecuteOnReceiveItemHooks(uint8_t item) { GameInteractor::Instance->ExecuteHooks(item); } -void GameInteractor_ExecuteOnSceneInitHooks(s16 sceneNum) { +void GameInteractor_ExecuteOnSceneInitHooks(int16_t sceneNum) { GameInteractor::Instance->ExecuteHooks(sceneNum); } @@ -43,3 +39,61 @@ void GameInteractor_ExecuteOnLoadFile(int32_t fileNum) { void GameInteractor_ExecuteOnDeleteFile(int32_t fileNum) { GameInteractor::Instance->ExecuteHooks(fileNum); } + +// MARK: - Dialog + +void GameInteractor_ExecuteOnDialogMessage() { + GameInteractor::Instance->ExecuteHooks(); +} + +void GameInteractor_ExecuteOnPresentTitleCard() { + GameInteractor::Instance->ExecuteHooks(); +} + +void GameInteractor_ExecuteOnInterfaceUpdate() { + GameInteractor::Instance->ExecuteHooks(); +} + +void GameInteractor_ExecuteOnKaleidoscopeUpdate(int16_t inDungeonScene) { + GameInteractor::Instance->ExecuteHooks(inDungeonScene); +} + +// MARK: - Main Menu + +void GameInteractor_ExecuteOnPresentFileSelect() { + GameInteractor::Instance->ExecuteHooks(); +} + +void GameInteractor_ExecuteOnUpdateFileSelectSelection(uint16_t optionIndex) { + GameInteractor::Instance->ExecuteHooks(optionIndex); +} + +void GameInteractor_ExecuteOnUpdateFileCopySelection(uint16_t optionIndex) { + GameInteractor::Instance->ExecuteHooks(optionIndex); +} + +void GameInteractor_ExecuteOnUpdateFileCopyConfirmationSelection(uint16_t optionIndex) { + GameInteractor::Instance->ExecuteHooks(optionIndex); +} + +void GameInteractor_ExecuteOnUpdateFileEraseSelection(uint16_t optionIndex) { + GameInteractor::Instance->ExecuteHooks(optionIndex); +} + +void GameInteractor_ExecuteOnUpdateFileEraseConfirmationSelection(uint16_t optionIndex) { + GameInteractor::Instance->ExecuteHooks(optionIndex); +} + +void GameInteractor_ExecuteOnUpdateFileAudioSelection(uint8_t optionIndex) { + GameInteractor::Instance->ExecuteHooks(optionIndex); +} + +void GameInteractor_ExecuteOnUpdateFileTargetSelection(uint8_t optionIndex) { + GameInteractor::Instance->ExecuteHooks(optionIndex); +} + +// MARK: - Game + +void GameInteractor_ExecuteOnSetGameLanguage() { + GameInteractor::Instance->ExecuteHooks(); +} diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h index 658ca05d4..2574306ab 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h @@ -4,11 +4,30 @@ extern "C" void GameInteractor_ExecuteOnLoadGame(int32_t fileNum); extern "C" void GameInteractor_ExecuteOnExitGame(int32_t fileNum); extern "C" void GameInteractor_ExecuteOnGameFrameUpdate(); -extern "C" void GameInteractor_ExecuteOnReceiveItemHooks(u8 item); -extern "C" void GameInteractor_ExecuteOnSceneInit(s16 sceneNum); +extern "C" void GameInteractor_ExecuteOnReceiveItemHooks(uint8_t item); +extern "C" void GameInteractor_ExecuteOnSceneInit(int16_t sceneNum); extern "C" void GameInteractor_ExecuteOnPlayerUpdate(); // MARK: - Save Files extern "C" void GameInteractor_ExecuteOnSaveFile(int32_t fileNum); extern "C" void GameInteractor_ExecuteOnLoadFile(int32_t fileNum); extern "C" void GameInteractor_ExecuteOnDeleteFile(int32_t fileNum); + +// MARK: - Dialog +extern "C" void GameInteractor_ExecuteOnDialogMessage(); +extern "C" void GameInteractor_ExecuteOnPresentTitleCard(); +extern "C" void GameInteractor_ExecuteOnInterfaceUpdate(); +extern "C" void GameInteractor_ExecuteOnKaleidoscopeUpdate(int16_t inDungeonScene); + +// MARK: - Main Menu +extern "C" void GameInteractor_ExecuteOnPresentFileSelect(); +extern "C" void GameInteractor_ExecuteOnUpdateFileSelectSelection(uint16_t optionIndex); +extern "C" void GameInteractor_ExecuteOnUpdateFileCopySelection(uint16_t optionIndex); +extern "C" void GameInteractor_ExecuteOnUpdateFileCopyConfirmationSelection(uint16_t optionIndex); +extern "C" void GameInteractor_ExecuteOnUpdateFileEraseSelection(uint16_t optionIndex); +extern "C" void GameInteractor_ExecuteOnUpdateFileEraseConfirmationSelection(uint16_t optionIndex); +extern "C" void GameInteractor_ExecuteOnUpdateFileAudioSelection(uint8_t optionIndex); +extern "C" void GameInteractor_ExecuteOnUpdateFileTargetSelection(uint8_t optionIndex); + +// MARK: - Game +extern "C" void GameInteractor_ExecuteOnSetGameLanguage(); diff --git a/soh/soh/Enhancements/mods.cpp b/soh/soh/Enhancements/mods.cpp index 66f2d169e..42b823992 100644 --- a/soh/soh/Enhancements/mods.cpp +++ b/soh/soh/Enhancements/mods.cpp @@ -1,6 +1,7 @@ #include "mods.h" #include #include "game-interactor/GameInteractor.h" +#include "tts/tts.h" extern "C" { #include @@ -262,6 +263,7 @@ void RegisterRupeeDash() { } void InitMods() { + RegisterTTS(); RegisterInfiniteMoney(); RegisterInfiniteHealth(); RegisterInfiniteAmmo(); diff --git a/soh/soh/Enhancements/speechsynthesizer/DarwinSpeechSynthesizer.h b/soh/soh/Enhancements/speechsynthesizer/DarwinSpeechSynthesizer.h new file mode 100644 index 000000000..623fd8e01 --- /dev/null +++ b/soh/soh/Enhancements/speechsynthesizer/DarwinSpeechSynthesizer.h @@ -0,0 +1,27 @@ +// +// DarwinSpeechSynthesizer.h +// libultraship +// +// Created by David Chavez on 22.11.22. +// + +#ifndef SOHDarwinSpeechSynthesizer_h +#define SOHDarwinSpeechSynthesizer_h + +#include "SpeechSynthesizer.h" + +class DarwinSpeechSynthesizer : public SpeechSynthesizer { + public: + DarwinSpeechSynthesizer(); + + void Speak(const char* text, const char* language); + + protected: + bool DoInit(void); + void DoUninitialize(void); + + private: + void* mSynthesizer; +}; + +#endif /* DarwinSpeechSynthesizer_h */ diff --git a/soh/soh/Enhancements/speechsynthesizer/DarwinSpeechSynthesizer.mm b/soh/soh/Enhancements/speechsynthesizer/DarwinSpeechSynthesizer.mm new file mode 100644 index 000000000..bd897e3dd --- /dev/null +++ b/soh/soh/Enhancements/speechsynthesizer/DarwinSpeechSynthesizer.mm @@ -0,0 +1,33 @@ +// +// DarwinSpeechSynthesizer.mm +// libultraship +// +// Created by David Chavez on 22.11.22. +// + +#include "DarwinSpeechSynthesizer.h" +#import + +DarwinSpeechSynthesizer::DarwinSpeechSynthesizer() {} + +bool DarwinSpeechSynthesizer::DoInit() { + mSynthesizer = (__bridge_retained void*)[[AVSpeechSynthesizer alloc] init]; + return true; +} + +void DarwinSpeechSynthesizer::DoUninitialize() { + [(__bridge AVSpeechSynthesizer *)mSynthesizer stopSpeakingAtBoundary:AVSpeechBoundaryImmediate]; + mSynthesizer = nil; +} + +void DarwinSpeechSynthesizer::Speak(const char* text, const char* language) { + AVSpeechUtterance *utterance = [AVSpeechUtterance speechUtteranceWithString:@(text)]; + [utterance setVoice:[AVSpeechSynthesisVoice voiceWithLanguage:@(language)]]; + + if (@available(macOS 11.0, *)) { + [utterance setPrefersAssistiveTechnologySettings:YES]; + } + + [(__bridge AVSpeechSynthesizer *)mSynthesizer stopSpeakingAtBoundary:AVSpeechBoundaryImmediate]; + [(__bridge AVSpeechSynthesizer *)mSynthesizer speakUtterance:utterance]; +} diff --git a/soh/soh/Enhancements/speechsynthesizer/SAPISpeechSynthesizer.cpp b/soh/soh/Enhancements/speechsynthesizer/SAPISpeechSynthesizer.cpp new file mode 100644 index 000000000..2be6b8e76 --- /dev/null +++ b/soh/soh/Enhancements/speechsynthesizer/SAPISpeechSynthesizer.cpp @@ -0,0 +1,56 @@ +// +// SAPISpeechSynthesizer.cpp +// libultraship +// +// Created by David Chavez on 22.11.22. +// + +#include "SAPISpeechSynthesizer.h" +#include +#include +#include +#include +#include + +ISpVoice* mVoice = NULL; + +SAPISpeechSynthesizer::SAPISpeechSynthesizer() { +} + +bool SAPISpeechSynthesizer::DoInit() { + CoInitializeEx(NULL, COINIT_MULTITHREADED); + HRESULT CoInitializeEx(LPVOID pvReserved, DWORD dwCoInit); + CoCreateInstance(CLSID_SpVoice, NULL, CLSCTX_ALL, IID_ISpVoice, (void**)&mVoice); + return true; +} + +void SAPISpeechSynthesizer::DoUninitialize() { + mVoice->Release(); + mVoice = NULL; + CoUninitialize(); +} + +std::wstring CharToWideString(std::string text) { + int textSize = MultiByteToWideChar(CP_UTF8, 0, &text[0], (int)text.size(), NULL, 0); + std::wstring wstrTo(textSize, 0); + MultiByteToWideChar(CP_UTF8, 0, &text[0], (int)text.size(), &wstrTo[0], textSize); + return wstrTo; +} + +void SpeakThreadTask(std::string text, std::string language) { + auto wText = CharToWideString(text); + auto wLanguage = CharToWideString(language); + + auto speakText = fmt::format( + L"{}", wLanguage, wText); + mVoice->Speak(speakText.c_str(), SPF_IS_XML | SPF_ASYNC | SPF_PURGEBEFORESPEAK, NULL); +} + +void SAPISpeechSynthesizer::Speak(const char* text, const char* language) { + // convert to string so char buffers don't have to be kept alive by caller + std::string textStr(text); + std::string languageStr(language); + + std::thread t1(SpeakThreadTask, textStr, languageStr); + t1.detach(); +} diff --git a/soh/soh/Enhancements/speechsynthesizer/SAPISpeechSynthesizer.h b/soh/soh/Enhancements/speechsynthesizer/SAPISpeechSynthesizer.h new file mode 100644 index 000000000..c5dfb59b9 --- /dev/null +++ b/soh/soh/Enhancements/speechsynthesizer/SAPISpeechSynthesizer.h @@ -0,0 +1,25 @@ +// +// SAPISpeechSynthesizer.h +// libultraship +// +// Created by David Chavez on 22.11.22. +// + +#ifndef SOHSAPISpeechSynthesizer_h +#define SOHSAPISpeechSynthesizer_h + +#include "SpeechSynthesizer.h" +#include + +class SAPISpeechSynthesizer : public SpeechSynthesizer { + public: + SAPISpeechSynthesizer(); + + void Speak(const char* text, const char* language); + + protected: + bool DoInit(void); + void DoUninitialize(void); +}; + +#endif /* SAPISpeechSynthesizer_h */ diff --git a/soh/soh/Enhancements/speechsynthesizer/SpeechSynthesizer.cpp b/soh/soh/Enhancements/speechsynthesizer/SpeechSynthesizer.cpp new file mode 100644 index 000000000..2b5f3cf77 --- /dev/null +++ b/soh/soh/Enhancements/speechsynthesizer/SpeechSynthesizer.cpp @@ -0,0 +1,32 @@ +// +// SpeechSynthesizer.cpp +// libultraship +// +// Created by David Chavez on 22.11.22. +// + +#include "SpeechSynthesizer.h" + +SpeechSynthesizer::SpeechSynthesizer() : mInitialized(false){}; + +bool SpeechSynthesizer::Init(void) { + if (mInitialized) { + return true; + } + + mInitialized = DoInit(); + return mInitialized; +} + +void SpeechSynthesizer::Uninitialize(void) { + if (!mInitialized) { + return; + } + + DoUninitialize(); + mInitialized = false; +} + +bool SpeechSynthesizer::IsInitialized(void) { + return mInitialized; +} diff --git a/soh/soh/Enhancements/speechsynthesizer/SpeechSynthesizer.h b/soh/soh/Enhancements/speechsynthesizer/SpeechSynthesizer.h new file mode 100644 index 000000000..b08aab05c --- /dev/null +++ b/soh/soh/Enhancements/speechsynthesizer/SpeechSynthesizer.h @@ -0,0 +1,38 @@ +// +// SpeechSynthesizer.h +// libultraship +// +// Created by David Chavez on 22.11.22. +// + +#ifndef SOHSpeechSynthesizer_h +#define SOHSpeechSynthesizer_h + +#include + +class SpeechSynthesizer { + public: + static SpeechSynthesizer* Instance; + SpeechSynthesizer(); + + bool Init(void); + void Uninitialize(void); + virtual void Speak(const char* text, const char* language) = 0; + + bool IsInitialized(void); + + protected: + virtual bool DoInit(void) = 0; + virtual void DoUninitialize(void) = 0; + + private: + bool mInitialized; +}; + +#endif /* SpeechSynthesizer_h */ + +#ifdef _WIN32 +#include "SAPISpeechSynthesizer.h" +#elif defined(__APPLE__) +#include "DarwinSpeechSynthesizer.h" +#endif diff --git a/soh/soh/Enhancements/tts/tts.cpp b/soh/soh/Enhancements/tts/tts.cpp new file mode 100644 index 000000000..013cf06d3 --- /dev/null +++ b/soh/soh/Enhancements/tts/tts.cpp @@ -0,0 +1,724 @@ +#include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/Enhancements/speechsynthesizer/SpeechSynthesizer.h" + +#include +#include +#include +#include + +#include "soh/OTRGlobals.h" +#include "message_data_static.h" +#include "overlays/gamestates/ovl_file_choose/file_choose.h" + +extern "C" { +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: { + return fileChooseMap[key].get(); + 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()) 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 uint16_t prevCursorIndex = 0; + static uint16_t prevCursorSpecialPos = 0; + static uint16_t prevCursorPoint[5] = { 0 }; + + PauseContext* pauseCtx = &gPlayState->pauseCtx; + Input* input = &gPlayState->state.input[0]; + + if (pauseCtx->state != 6) { + //reset cursor index to so it is announced when pause is reopened + prevCursorIndex = -1; + return; + } + + if ((pauseCtx->debugState != 1) && (pauseCtx->debugState != 2)) { + char arg[8]; + if (CHECK_BTN_ALL(input->press.button, BTN_DUP)) { + snprintf(arg, sizeof(arg), "%d", gSaveContext.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)) { + snprintf(arg, sizeof(arg), "%d", gSaveContext.magic); + 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; + } + + 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: + snprintf(arg, sizeof(arg), "%d", AMMO(pauseCtx->cursorItem[PAUSE_ITEM])); + break; + case ITEM_BEAN: + snprintf(arg, sizeof(arg), "%d", 0); + break; + default: + arg[0] = '\0'; + } + + if (pauseCtx->cursorItem[PAUSE_ITEM] == 999) { + return; + } + + std::string key = std::to_string(pauseCtx->cursorItem[PAUSE_ITEM]); + auto translation = GetParameritizedText(key, TEXT_BANK_KALEIDO, arg); + SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode()); + break; + } + case PAUSE_MAP: + if (inDungeonScene) { + 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 { + 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()); + SPDLOG_INFO("Item: {}", key); + } + 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 & 0xF) & 0xF) >> 0x1C); + break; + default: + arg[0] = '\0'; + } + + if (pauseCtx->cursorItem[PAUSE_QUEST] == 999) { + 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: + { + std::string key = std::to_string(pauseCtx->cursorItem[PAUSE_EQUIP]); + auto translation = GetParameritizedText(key, TEXT_BANK_KALEIDO, nullptr); + SpeechSynthesizer::Instance->Speak(translation.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_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; + } + + }); +} + +// 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 = OTRGlobals::Instance->context->GetResourceManager()->LoadFile("accessibility/texts/scenes" + languageSuffix); + if (sceneFile != nullptr) { + sceneMap = nlohmann::json::parse(sceneFile->Buffer, nullptr, true, true); + } + + auto miscFile = OTRGlobals::Instance->context->GetResourceManager()->LoadFile("accessibility/texts/misc" + languageSuffix); + if (miscFile != nullptr) { + miscMap = nlohmann::json::parse(miscFile->Buffer, nullptr, true, true); + } + + auto kaleidoFile = OTRGlobals::Instance->context->GetResourceManager()->LoadFile("accessibility/texts/kaleidoscope" + languageSuffix); + if (kaleidoFile != nullptr) { + kaleidoMap = nlohmann::json::parse(kaleidoFile->Buffer, nullptr, true, true); + } + + auto fileChooseFile = OTRGlobals::Instance->context->GetResourceManager()->LoadFile("accessibility/texts/filechoose" + languageSuffix); + if (fileChooseFile != nullptr) { + fileChooseMap = nlohmann::json::parse(fileChooseFile->Buffer, nullptr, true, true); + } +} + +void RegisterOnSetGameLanguageHook() { + GameInteractor::Instance->RegisterGameHook([]() { + InitTTSBank(); + }); +} + +void RegisterTTSModHooks() { + RegisterOnSetGameLanguageHook(); + RegisterOnDialogMessageHook(); + RegisterOnSceneInitHook(); + RegisterOnPresentTitleCardHook(); + RegisterOnInterfaceUpdateHook(); + RegisterOnKaleidoscopeUpdateHook(); + RegisterOnUpdateMainMenuSelection(); +} + +void RegisterTTS() { + InitTTSBank(); + RegisterTTSModHooks(); +} diff --git a/soh/soh/Enhancements/tts/tts.h b/soh/soh/Enhancements/tts/tts.h new file mode 100644 index 000000000..427628704 --- /dev/null +++ b/soh/soh/Enhancements/tts/tts.h @@ -0,0 +1,6 @@ +#ifndef TTS_H +#define TTS_H + +void RegisterTTS(); + +#endif diff --git a/soh/soh/GameMenuBar.cpp b/soh/soh/GameMenuBar.cpp index d10071980..c475148ac 100644 --- a/soh/soh/GameMenuBar.cpp +++ b/soh/soh/GameMenuBar.cpp @@ -33,6 +33,8 @@ #include "Enhancements/crowd-control/CrowdControl.h" #endif +#include "Enhancements/game-interactor/GameInteractor.h" + #define EXPERIMENTAL() \ ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 50, 50, 255)); \ UIWidgets::Spacer(3.0f); \ @@ -297,15 +299,25 @@ namespace GameMenuBar { if (ImGui::BeginMenu("Languages")) { UIWidgets::PaddedEnhancementCheckbox("Translate Title Screen", "gTitleScreenTranslation"); - UIWidgets::EnhancementRadioButton("English", "gLanguages", LANGUAGE_ENG); - UIWidgets::EnhancementRadioButton("German", "gLanguages", LANGUAGE_GER); - UIWidgets::EnhancementRadioButton("French", "gLanguages", LANGUAGE_FRA); + if (UIWidgets::EnhancementRadioButton("English", "gLanguages", LANGUAGE_ENG)) { + GameInteractor::Instance->ExecuteHooks(); + } + if (UIWidgets::EnhancementRadioButton("German", "gLanguages", LANGUAGE_GER)) { + GameInteractor::Instance->ExecuteHooks(); + } + if (UIWidgets::EnhancementRadioButton("French", "gLanguages", LANGUAGE_FRA)) { + GameInteractor::Instance->ExecuteHooks(); + } ImGui::EndMenu(); } UIWidgets::Spacer(0); if (ImGui::BeginMenu("Accessibility")) { + #if defined(_WIN32) || defined(__APPLE__) + UIWidgets::PaddedEnhancementCheckbox("Text to Speech", "gA11yTTS"); + UIWidgets::Tooltip("Enables text to speech for in game dialog"); + #endif UIWidgets::PaddedEnhancementCheckbox("Disable Idle Camera Re-Centering", "gA11yDisableIdleCam"); UIWidgets::Tooltip("Disables the automatic re-centering of the camera when idle."); diff --git a/soh/soh/OTRGlobals.cpp b/soh/soh/OTRGlobals.cpp index 865237094..cd4916c7d 100644 --- a/soh/soh/OTRGlobals.cpp +++ b/soh/soh/OTRGlobals.cpp @@ -1,4 +1,4 @@ -#include "OTRGlobals.h" +#include "OTRGlobals.h" #include "OTRAudio.h" #include #include @@ -27,6 +27,7 @@ #define DRWAV_IMPLEMENTATION #include #include +#include "Enhancements/speechsynthesizer/SpeechSynthesizer.h" #include "Enhancements/controls/GameControlEditor.h" #include "Enhancements/cosmetics/CosmeticsEditor.h" #include "Enhancements/audio/AudioCollection.h" @@ -111,6 +112,7 @@ CustomMessageManager* CustomMessageManager::Instance; ItemTableManager* ItemTableManager::Instance; GameInteractor* GameInteractor::Instance; AudioCollection* AudioCollection::Instance; +SpeechSynthesizer* SpeechSynthesizer::Instance; extern "C" char** cameraStrings; std::vector> cameraStdStrings; @@ -579,7 +581,14 @@ extern "C" void InitOTR() { ItemTableManager::Instance = new ItemTableManager(); GameInteractor::Instance = new GameInteractor(); AudioCollection::Instance = new AudioCollection(); - +#ifdef __APPLE__ + SpeechSynthesizer::Instance = new DarwinSpeechSynthesizer(); + SpeechSynthesizer::Instance->Init(); +#elif defined(_WIN32) + SpeechSynthesizer::Instance = new SAPISpeechSynthesizer(); + SpeechSynthesizer::Instance->Init(); +#endif + clearMtx = (uintptr_t)&gMtxClear; OTRMessage_Init(); OTRAudio_Init(); @@ -618,6 +627,9 @@ extern "C" void InitOTR() { extern "C" void DeinitOTR() { OTRAudio_Exit(); +#if defined(_WIN32) || defined(__APPLE__) + SpeechSynthesizerUninitialize(); +#endif #ifdef ENABLE_CROWD_CONTROL CrowdControl::Instance->Disable(); CrowdControl::Instance->Shutdown(); @@ -718,6 +730,10 @@ extern "C" void Graph_StartFrame() { break; } + case SDL_SCANCODE_F9: { + // Toggle TTS + CVarSetInteger("gA11yTTS", !CVarGetInteger("gA11yTTS", 0)); + } } #endif OTRGlobals::Instance->context->StartFrame(); diff --git a/soh/soh/UIWidgets.cpp b/soh/soh/UIWidgets.cpp index 70bf47209..d0c2079bb 100644 --- a/soh/soh/UIWidgets.cpp +++ b/soh/soh/UIWidgets.cpp @@ -513,7 +513,7 @@ namespace UIWidgets { Spacer(0); } - void EnhancementRadioButton(const char* text, const char* cvarName, int id) { + bool EnhancementRadioButton(const char* text, const char* cvarName, int id) { /*Usage : EnhancementRadioButton("My Visible Name","gMyCVarName", MyID); First arg is the visible name of the Radio button @@ -528,13 +528,17 @@ namespace UIWidgets { make_invisible += text; make_invisible += cvarName; + bool ret = false; int val = CVarGetInteger(cvarName, 0); if (ImGui::RadioButton(make_invisible.c_str(), id == val)) { CVarSetInteger(cvarName, id); SohImGui::RequestCvarSaveOnNextTick(); + ret = true; } ImGui::SameLine(); ImGui::Text("%s", text); + + return ret; } bool DrawResetColorButton(const char* cvarName, ImVec4* colors, ImVec4 defaultcolors, bool has_alpha) { diff --git a/soh/soh/UIWidgets.hpp b/soh/soh/UIWidgets.hpp index 4cb5b1012..538377bb2 100644 --- a/soh/soh/UIWidgets.hpp +++ b/soh/soh/UIWidgets.hpp @@ -62,7 +62,7 @@ namespace UIWidgets { bool EnhancementSliderInt(const char* text, const char* id, const char* cvarName, int min, int max, const char* format, int defaultValue = 0, bool PlusMinusButton = false, bool disabled = false, const char* disabledTooltipText = ""); void PaddedEnhancementSliderInt(const char* text, const char* id, const char* cvarName, int min, int max, const char* format, int defaultValue = 0, bool PlusMinusButton = false, bool padTop = true, bool padBottom = true, bool disabled = false, const char* disabledTooltipText = ""); bool EnhancementSliderFloat(const char* text, const char* id, const char* cvarName, float min, float max, const char* format, float defaultValue, bool isPercentage, bool PlusMinusButton = false, bool disabled = false, const char* disabledTooltipText = ""); - void EnhancementRadioButton(const char* text, const char* cvarName, int id); + bool EnhancementRadioButton(const char* text, const char* cvarName, int id); bool EnhancementColor(const char* text, const char* cvarName, ImVec4 ColorRGBA, ImVec4 default_colors, bool allow_rainbow = true, bool has_alpha=false, bool TitleSameLine=false); void DrawFlagArray32(const std::string& name, uint32_t& flags); diff --git a/soh/src/code/z_actor.c b/soh/src/code/z_actor.c index 5a4010dda..9394c3b8a 100644 --- a/soh/src/code/z_actor.c +++ b/soh/src/code/z_actor.c @@ -1020,8 +1020,6 @@ void TitleCard_InitPlaceName(PlayState* play, TitleCardContext* titleCtx, void* } titleCtx->texture = GetResourceDataByName(texture, false); - - //titleCtx->texture = texture; titleCtx->isBossCard = false; titleCtx->hasTranslation = false; titleCtx->x = x; @@ -1044,6 +1042,10 @@ void TitleCard_Update(PlayState* play, TitleCardContext* titleCtx) { } if (DECR(titleCtx->delayTimer) == 0) { + if (titleCtx->durationTimer == 80) { + GameInteractor_ExecuteOnPresentTitleCard(); + } + if (DECR(titleCtx->durationTimer) == 0) { Math_StepToS(&titleCtx->alpha, 0, 30); Math_StepToS(&titleCtx->intensityR, 0, 70); diff --git a/soh/src/code/z_message_PAL.c b/soh/src/code/z_message_PAL.c index 922832571..ec3eff08d 100644 --- a/soh/src/code/z_message_PAL.c +++ b/soh/src/code/z_message_PAL.c @@ -3133,6 +3133,8 @@ void Message_Update(PlayState* play) { if (msgCtx->msgLength == 0) { return; } + + GameInteractor_ExecuteOnDialogMessage(); bool isB_Held = CVarGetInteger("gSkipText", 0) != 0 ? CHECK_BTN_ALL(input->cur.button, BTN_B) && !sTextboxSkipped : CHECK_BTN_ALL(input->press.button, BTN_B); diff --git a/soh/src/code/z_parameter.c b/soh/src/code/z_parameter.c index df8e40869..06d23c151 100644 --- a/soh/src/code/z_parameter.c +++ b/soh/src/code/z_parameter.c @@ -6064,6 +6064,8 @@ void Interface_Update(PlayState* play) { Left_HUD_Margin = CVarGetInteger("gHUDMargin_L", 0); Right_HUD_Margin = CVarGetInteger("gHUDMargin_R", 0); Bottom_HUD_Margin = CVarGetInteger("gHUDMargin_B", 0); + + GameInteractor_ExecuteOnInterfaceUpdate(); if (CHECK_BTN_ALL(debugInput->press.button, BTN_DLEFT)) { gSaveContext.language = LANGUAGE_ENG; diff --git a/soh/src/overlays/gamestates/ovl_file_choose/file_choose.h b/soh/src/overlays/gamestates/ovl_file_choose/file_choose.h index 551bd3942..b926d7fd9 100644 --- a/soh/src/overlays/gamestates/ovl_file_choose/file_choose.h +++ b/soh/src/overlays/gamestates/ovl_file_choose/file_choose.h @@ -148,6 +148,11 @@ typedef enum { /* 3 */ FS_AUDIO_SURROUND } AudioOption; +typedef enum { + /* 0 */ FS_TARGET_SWITCH, + /* 1 */ FS_TARGET_HOLD, +} TargetOption; + typedef enum { /* 0 */ FS_CHAR_PAGE_HIRA, /* 1 */ FS_CHAR_PAGE_KATA, @@ -209,8 +214,8 @@ void FileChoose_DrawNameEntry(GameState* thisx); void FileChoose_DrawCharacter(GraphicsContext* gfxCtx, void* texture, s16 vtx); void HandleMouseInput(Input* input); -u8 HandleMouseCursor(FileChooseContext* this, Input* input, int minx, int miny, int maxx, int maxy); -Vec2f HandleMouseCursorSplit(FileChooseContext* this, Input* input, int minx, int miny, int maxx, int maxy, int countx, +u8 HandleMouseCursor(FileChooseContext* thisx, Input* input, int minx, int miny, int maxx, int maxy); +Vec2f HandleMouseCursorSplit(FileChooseContext* thisx, Input* input, int minx, int miny, int maxx, int maxy, int countx, int county); extern s16 D_808123F0[]; diff --git a/soh/src/overlays/gamestates/ovl_file_choose/z_file_choose.c b/soh/src/overlays/gamestates/ovl_file_choose/z_file_choose.c index 2823311e1..316956de3 100644 --- a/soh/src/overlays/gamestates/ovl_file_choose/z_file_choose.c +++ b/soh/src/overlays/gamestates/ovl_file_choose/z_file_choose.c @@ -349,6 +349,7 @@ void FileChoose_FinishFadeIn(GameState* thisx) { this->controlsAlpha = 255; this->windowAlpha = 200; this->configMode = CM_MAIN_MENU; + GameInteractor_ExecuteOnPresentFileSelect(); } } @@ -478,6 +479,8 @@ void FileChoose_UpdateRandomizer() { } } +uint16_t lastFileChooseButtonIndex; + /** * Update the cursor and wait for the player to select a button to change menus accordingly. * If an empty file is selected, enter the name entry config mode. @@ -597,6 +600,11 @@ void FileChoose_UpdateMainMenu(GameState* thisx) { } else { this->warningLabel = FS_WARNING_NONE; } + + if (lastFileChooseButtonIndex != this->buttonIndex) { + GameInteractor_ExecuteOnUpdateFileSelectSelection(this->buttonIndex); + lastFileChooseButtonIndex = this->buttonIndex; + } } } diff --git a/soh/src/overlays/gamestates/ovl_file_choose/z_file_copy_erase.c b/soh/src/overlays/gamestates/ovl_file_choose/z_file_copy_erase.c index 77ca745f8..da4e72a8e 100644 --- a/soh/src/overlays/gamestates/ovl_file_choose/z_file_copy_erase.c +++ b/soh/src/overlays/gamestates/ovl_file_choose/z_file_copy_erase.c @@ -54,6 +54,8 @@ void FileChoose_SetupCopySource(GameState* thisx) { } } +uint16_t lastCopyEraseButtonIndex; + /** * Allow the player to select a file to copy or exit back to the main menu. * Update function for `CM_SELECT_COPY_SOURCE` @@ -110,6 +112,11 @@ void FileChoose_SelectCopySource(GameState* thisx) { } } } + + if (lastCopyEraseButtonIndex != this->buttonIndex) { + GameInteractor_ExecuteOnUpdateFileCopySelection(this->buttonIndex); + lastCopyEraseButtonIndex = this->buttonIndex; + } } /** @@ -379,6 +386,11 @@ void FileChoose_CopyConfirm(GameState* thisx) { Audio_PlaySoundGeneral(NA_SE_SY_FSEL_CURSOR, &D_801333D4, 4, &D_801333E0, &D_801333E0, &D_801333E8); this->buttonIndex ^= 1; } + + if (lastCopyEraseButtonIndex != this->buttonIndex) { + GameInteractor_ExecuteOnUpdateFileCopyConfirmationSelection(this->buttonIndex); + lastCopyEraseButtonIndex = this->buttonIndex; + } } /** @@ -724,6 +736,11 @@ void FileChoose_EraseSelect(GameState* thisx) { this->warningLabel = FS_WARNING_NONE; } } + + if (lastCopyEraseButtonIndex != this->buttonIndex) { + GameInteractor_ExecuteOnUpdateFileEraseSelection(this->buttonIndex); + lastCopyEraseButtonIndex = this->buttonIndex; + } } /** @@ -833,6 +850,11 @@ void FileChoose_EraseConfirm(GameState* thisx) { Audio_PlaySoundGeneral(NA_SE_SY_FSEL_CURSOR, &D_801333D4, 4, &D_801333E0, &D_801333E0, &D_801333E8); this->buttonIndex ^= 1; } + + if (lastCopyEraseButtonIndex != this->buttonIndex) { + GameInteractor_ExecuteOnUpdateFileEraseConfirmationSelection(this->buttonIndex); + lastCopyEraseButtonIndex = this->buttonIndex; + } } /** diff --git a/soh/src/overlays/gamestates/ovl_file_choose/z_file_nameset_PAL.c b/soh/src/overlays/gamestates/ovl_file_choose/z_file_nameset_PAL.c index 999a62a91..6ad5afb7e 100644 --- a/soh/src/overlays/gamestates/ovl_file_choose/z_file_nameset_PAL.c +++ b/soh/src/overlays/gamestates/ovl_file_choose/z_file_nameset_PAL.c @@ -656,6 +656,7 @@ void FileChoose_StartOptions(GameState* thisx) { } static u8 sSelectedSetting; +int8_t lastOptionButtonIndex = -1; /** * Update the cursor and appropriate settings for the options menu. @@ -718,6 +719,19 @@ void FileChoose_UpdateOptionsMenu(GameState* thisx) { Audio_PlaySoundGeneral(NA_SE_SY_FSEL_DECIDE_L, &D_801333D4, 4, &D_801333E0, &D_801333E0, &D_801333E8); sSelectedSetting ^= 1; } + + if (sSelectedSetting == FS_SETTING_AUDIO) { + if (lastOptionButtonIndex != gSaveContext.audioSetting) { + GameInteractor_ExecuteOnUpdateFileAudioSelection(gSaveContext.audioSetting); + lastOptionButtonIndex = gSaveContext.audioSetting; + } + } else { + // offset to detect switching between modes + if (lastOptionButtonIndex != FS_BTN_SELECT_QUIT + gSaveContext.zTargetSetting + 1) { + GameInteractor_ExecuteOnUpdateFileTargetSelection(gSaveContext.zTargetSetting); + lastOptionButtonIndex = FS_BTN_SELECT_QUIT + gSaveContext.zTargetSetting + 1; + } + } } typedef struct { diff --git a/soh/src/overlays/misc/ovl_kaleido_scope/z_kaleido_scope_PAL.c b/soh/src/overlays/misc/ovl_kaleido_scope/z_kaleido_scope_PAL.c index 63db7a5b1..199889679 100644 --- a/soh/src/overlays/misc/ovl_kaleido_scope/z_kaleido_scope_PAL.c +++ b/soh/src/overlays/misc/ovl_kaleido_scope/z_kaleido_scope_PAL.c @@ -4260,4 +4260,6 @@ void KaleidoScope_Update(PlayState* play) osSyncPrintf(VT_RST); break; } + + GameInteractor_ExecuteOnKaleidoscopeUpdate(sInDungeonScene); }