├── .gitignore ├── Changelog ├── 0.9.5.txt ├── 0.9.6.txt ├── 0.9.7.txt ├── 1.0.0.txt ├── 1.0.1.txt ├── 1.0.4.txt └── 1.1.1.txt ├── LICENSE ├── README.md ├── SpeechMod.sln ├── SpeechMod ├── Configuration │ ├── ModConfigurationManager.cs │ ├── Settings │ │ ├── ModHotkeySettingEntry.cs │ │ ├── ModSettingEntry.cs │ │ ├── ModToggleSettingEntry.cs │ │ └── SettingStatus.cs │ └── UI │ │ └── OwlcatUITools.cs ├── Constants.cs ├── Info.json ├── Keybinds │ └── PlaybackStop.cs ├── Localization │ ├── ModLocalizationManager.cs │ └── enGB.json ├── Main.cs ├── Patches │ ├── BookEventView_Patch.cs │ ├── CharInfoAlignmentHistoryRecordView_Patch.cs │ ├── CharInfoAlignmentWheelPCView_Patch.cs │ ├── CharInfoCompanionStoryFullView_Patch.cs │ ├── CombatResultView_Patch.cs │ ├── DialogAnswerView_Patch.cs │ ├── DialogController_Patch.cs │ ├── Dialog_Patch.cs │ ├── Encyclopedia_Patch.cs │ ├── GlobalMapEnterMessage_Patch.cs │ ├── JournalQuestObjective_Patch.cs │ ├── LoadingScreenPCView_Patch.cs │ ├── LocalMapBaseView_Patch.cs │ ├── MessageModal_Patch.cs │ ├── NewGamePhaseStoryPCView_Patch.cs │ ├── SettingsEntityDropdownGameDifficultyItemPCView_Patch.cs │ ├── SettingsEntityView_Patch.cs │ ├── StaticCanvas_Patch.cs │ ├── TooltipEngine_Patch.cs │ └── TutorialWindowView_Patch.cs ├── PhoneticDictionary.json ├── Settings.cs ├── SpeechMod.csproj ├── Unity │ ├── AppleVoiceUnity.cs │ ├── ButtonFactory.cs │ ├── Extensions │ │ ├── Hooks.cs │ │ ├── Transforms.cs │ │ └── UIHelper.cs │ ├── MenuGUI.cs │ ├── TextMeshProHookData.cs │ └── WindowsVoiceUnity.cs ├── Voice │ ├── AppleSpeech.cs │ ├── Enums.cs │ ├── ISpeech.cs │ ├── PhoneticDictionary.cs │ ├── SpeechExtensions.cs │ └── WindowsSpeech.cs └── packages.config ├── Test ├── StringManipulationTests.cs └── Test.csproj ├── Todo.txt └── WindowsVoice ├── WindowsVoice.cs ├── WindowsVoice.h ├── WindowsVoice.sln ├── WindowsVoice.vcxproj ├── dllmain.cpp ├── pch.cpp └── pch.h /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | [Ll]ib/ 13 | [Ww]eb[Rr]esources/ 14 | 15 | # User-specific files (MonoDevelop/Xamarin Studio) 16 | *.userprefs 17 | 18 | # Build results 19 | [Dd]ebug/ 20 | [Dd]ebugPublic/ 21 | [Rr]elease/ 22 | [Rr]eleases/ 23 | x64/ 24 | x86/ 25 | [Aa][Rr][Mm]/ 26 | [Aa][Rr][Mm]64/ 27 | bld/ 28 | [Bb]in/ 29 | [Oo]bj/ 30 | [Ll]og/ 31 | 32 | # Visual Studio 2015/2017 cache/options directory 33 | .vs/ 34 | # Uncomment if you have tasks that create the project's static files in wwwroot 35 | #wwwroot/ 36 | 37 | # Visual Studio 2017 auto generated files 38 | Generated\ Files/ 39 | 40 | # MSTest test Results 41 | [Tt]est[Rr]esult*/ 42 | [Bb]uild[Ll]og.* 43 | 44 | # NUNIT 45 | *.VisualState.xml 46 | TestResult.xml 47 | 48 | # Build Results of an ATL Project 49 | [Dd]ebugPS/ 50 | [Rr]eleasePS/ 51 | [Ll]ibs/ 52 | dlldata.c 53 | 54 | # Benchmark Results 55 | BenchmarkDotNet.Artifacts/ 56 | 57 | # .NET Core 58 | project.lock.json 59 | project.fragment.lock.json 60 | artifacts/ 61 | 62 | # StyleCop 63 | StyleCopReport.xml 64 | 65 | # Files built by Visual Studio 66 | *_i.c 67 | *_p.c 68 | *_h.h 69 | *.ilk 70 | *.meta 71 | *.obj 72 | *.iobj 73 | *.pch 74 | *.pdb 75 | *.ipdb 76 | *.pgc 77 | *.pgd 78 | *.rsp 79 | *.sbr 80 | *.tlb 81 | *.tli 82 | *.tlh 83 | *.tmp 84 | *.tmp_proj 85 | *_wpftmp.csproj 86 | *.log 87 | *.vspscc 88 | *.vssscc 89 | .builds 90 | *.pidb 91 | *.svclog 92 | *.scc 93 | 94 | # Chutzpah Test files 95 | _Chutzpah* 96 | 97 | # Visual C++ cache files 98 | ipch/ 99 | *.aps 100 | *.ncb 101 | *.opendb 102 | *.opensdf 103 | *.sdf 104 | *.cachefile 105 | *.VC.db 106 | *.VC.VC.opendb 107 | 108 | # Visual Studio profiler 109 | *.psess 110 | *.vsp 111 | *.vspx 112 | *.sap 113 | 114 | # Visual Studio Trace Files 115 | *.e2e 116 | 117 | # TFS 2012 Local Workspace 118 | $tf/ 119 | 120 | # Guidance Automation Toolkit 121 | *.gpState 122 | 123 | # ReSharper is a .NET coding add-in 124 | _ReSharper*/ 125 | *.[Rr]e[Ss]harper 126 | *.DotSettings.user 127 | 128 | # JustCode is a .NET coding add-in 129 | .JustCode 130 | 131 | # TeamCity is a build add-in 132 | _TeamCity* 133 | 134 | # DotCover is a Code Coverage Tool 135 | *.dotCover 136 | 137 | # AxoCover is a Code Coverage Tool 138 | .axoCover/* 139 | !.axoCover/settings.json 140 | 141 | # Visual Studio code coverage results 142 | *.coverage 143 | *.coveragexml 144 | 145 | # NCrunch 146 | _NCrunch_* 147 | .*crunch*.local.xml 148 | nCrunchTemp_* 149 | 150 | # MightyMoose 151 | *.mm.* 152 | AutoTest.Net/ 153 | 154 | # Web workbench (sass) 155 | .sass-cache/ 156 | 157 | # Installshield output folder 158 | [Ee]xpress/ 159 | 160 | # DocProject is a documentation generator add-in 161 | DocProject/buildhelp/ 162 | DocProject/Help/*.HxT 163 | DocProject/Help/*.HxC 164 | DocProject/Help/*.hhc 165 | DocProject/Help/*.hhk 166 | DocProject/Help/*.hhp 167 | DocProject/Help/Html2 168 | DocProject/Help/html 169 | 170 | # Click-Once directory 171 | publish/ 172 | 173 | # Publish Web Output 174 | *.[Pp]ublish.xml 175 | *.azurePubxml 176 | # Note: Comment the next line if you want to checkin your web deploy settings, 177 | # but database connection strings (with potential passwords) will be unencrypted 178 | *.pubxml 179 | *.publishproj 180 | 181 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 182 | # checkin your Azure Web App publish settings, but sensitive information contained 183 | # in these scripts will be unencrypted 184 | PublishScripts/ 185 | 186 | # NuGet Packages 187 | *.nupkg 188 | # The packages folder can be ignored because of Package Restore 189 | **/[Pp]ackages/* 190 | # except build/, which is used as an MSBuild target. 191 | !**/[Pp]ackages/build/ 192 | # Uncomment if necessary however generally it will be regenerated when needed 193 | #!**/[Pp]ackages/repositories.config 194 | # NuGet v3's project.json files produces more ignorable files 195 | *.nuget.props 196 | *.nuget.targets 197 | 198 | # Microsoft Azure Build Output 199 | csx/ 200 | *.build.csdef 201 | 202 | # Microsoft Azure Emulator 203 | ecf/ 204 | rcf/ 205 | 206 | # Windows Store app package directories and files 207 | AppPackages/ 208 | BundleArtifacts/ 209 | Package.StoreAssociation.xml 210 | _pkginfo.txt 211 | *.appx 212 | 213 | # Visual Studio cache files 214 | # files ending in .cache can be ignored 215 | *.[Cc]ache 216 | # but keep track of directories ending in .cache 217 | !?*.[Cc]ache/ 218 | 219 | # Others 220 | ClientBin/ 221 | ~$* 222 | *~ 223 | *.dbmdl 224 | *.dbproj.schemaview 225 | *.jfm 226 | *.pfx 227 | *.publishsettings 228 | orleans.codegen.cs 229 | 230 | # Including strong name files can present a security risk 231 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 232 | #*.snk 233 | 234 | # Since there are multiple workflows, uncomment next line to ignore bower_components 235 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 236 | #bower_components/ 237 | 238 | # RIA/Silverlight projects 239 | Generated_Code/ 240 | 241 | # Backup & report files from converting an old project file 242 | # to a newer Visual Studio version. Backup files are not needed, 243 | # because we have git ;-) 244 | _UpgradeReport_Files/ 245 | Backup*/ 246 | UpgradeLog*.XML 247 | UpgradeLog*.htm 248 | ServiceFabricBackup/ 249 | *.rptproj.bak 250 | 251 | # SQL Server files 252 | *.mdf 253 | *.ldf 254 | *.ndf 255 | 256 | # Business Intelligence projects 257 | *.rdl.data 258 | *.bim.layout 259 | *.bim_*.settings 260 | *.rptproj.rsuser 261 | *- Backup*.rdl 262 | 263 | # Microsoft Fakes 264 | FakesAssemblies/ 265 | 266 | # GhostDoc plugin setting file 267 | *.GhostDoc.xml 268 | 269 | # Node.js Tools for Visual Studio 270 | .ntvs_analysis.dat 271 | node_modules/ 272 | 273 | # Visual Studio 6 build log 274 | *.plg 275 | 276 | # Visual Studio 6 workspace options file 277 | *.opt 278 | 279 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 280 | *.vbw 281 | 282 | # Visual Studio LightSwitch build output 283 | **/*.HTMLClient/GeneratedArtifacts 284 | **/*.DesktopClient/GeneratedArtifacts 285 | **/*.DesktopClient/ModelManifest.xml 286 | **/*.Server/GeneratedArtifacts 287 | **/*.Server/ModelManifest.xml 288 | _Pvt_Extensions 289 | 290 | # Paket dependency manager 291 | .paket/paket.exe 292 | paket-files/ 293 | 294 | # FAKE - F# Make 295 | .fake/ 296 | 297 | # JetBrains Rider 298 | .idea/ 299 | *.sln.iml 300 | 301 | # CodeRush personal settings 302 | .cr/personal 303 | 304 | # Python Tools for Visual Studio (PTVS) 305 | __pycache__/ 306 | *.pyc 307 | 308 | # Cake - Uncomment if you are using it 309 | # tools/** 310 | # !tools/packages.config 311 | 312 | # Tabs Studio 313 | *.tss 314 | 315 | # Telerik's JustMock configuration file 316 | *.jmconfig 317 | 318 | # BizTalk build output 319 | *.btp.cs 320 | *.btm.cs 321 | *.odx.cs 322 | *.xsd.cs 323 | 324 | # OpenCover UI analysis results 325 | OpenCover/ 326 | 327 | # Azure Stream Analytics local run output 328 | ASALocalRun/ 329 | 330 | # MSBuild Binary and Structured Log 331 | *.binlog 332 | 333 | # NVidia Nsight GPU debugger configuration file 334 | *.nvuser 335 | 336 | # MFractors (Xamarin productivity tool) working folder 337 | .mfractor/ 338 | 339 | # Local History for Visual Studio 340 | .localhistory/ 341 | 342 | # BeatPulse healthcheck temp database 343 | healthchecksdb 344 | 345 | # BuildArtifacts 346 | GamePath.props 347 | -------------------------------------------------------------------------------- /Changelog/0.9.5.txt: -------------------------------------------------------------------------------- 1 | SpeechMod 2 | 3 | Version 0.9.5 Changelog 4 | 5 | - Added a slider to adjust the pitch of the voice. 6 | - Voices to choose from in the settings now correctly displays current available voices. 7 | - Added play buttons to Journal Quest entries. 8 | - Added play buttons to Encyclopedia entries. 9 | NOTE: There'a currently a bug in the base game where sub-menus in the Encyclopedia disappears at times, this is not due to this mod. 10 | - List of phonetical replacements is now stored in a json file; PhoneticDictionary.json. 11 | The dictionary is case sensitive. 12 | Can be reloaded from the UMM while playing. 13 | - Added and updated some words in the phonetic dictionary. 14 | 15 | Report issues here thanks https://github.com/Osmodium/PathfinderTextToSpeechMod/issues -------------------------------------------------------------------------------- /Changelog/0.9.6.txt: -------------------------------------------------------------------------------- 1 | SpeechMod 2 | 3 | Version 0.9.6 Changelog 4 | 5 | - Added support for book-events. 6 | - Added support for message boxes. 7 | - Fix encyclopedia creature page bug. 8 | - Added and updated some words in the phonetic dictionary. 9 | - Various slight optimizations and stability fixes. 10 | 11 | Report issues here thanks https://github.com/Osmodium/PathfinderTextToSpeechMod/issues -------------------------------------------------------------------------------- /Changelog/0.9.7.txt: -------------------------------------------------------------------------------- 1 | SpeechMod 2 | 3 | Version 0.9.7 Changelog 4 | 5 | - Fix book events when traveling on the map. 6 | - Added and updated some words in the phonetic dictionary. 7 | - Various slight optimizations and stability fixes. 8 | 9 | Report issues here thanks https://github.com/Osmodium/PathfinderTextToSpeechMod/issues -------------------------------------------------------------------------------- /Changelog/1.0.0.txt: -------------------------------------------------------------------------------- 1 | SpeechMod 2 | 3 | Version 1.0.0 Changelog 4 | 5 | - Added Auto play feature, of dialogs when it's not voice acted. I guess it can be called synthetic voice acting. 6 | - Added gender specific voices for dialog feature, with individual voice rate, pitch and volume where available. 7 | - Added text and button to preview the selected voice in the settings. 8 | - Added support for macOS, using the voices available through the 'say' command! No longer Windows only woo! (I guess only Linux remains) 9 | - Various slight optimizations and stability fixes. 10 | 11 | Report issues here thanks https://github.com/Osmodium/PathfinderTextToSpeechMod/issues -------------------------------------------------------------------------------- /Changelog/1.0.1.txt: -------------------------------------------------------------------------------- 1 | SpeechMod 2 | 3 | Version 1.0.1 Changelog 4 | 5 | - Various slight optimizations and stability fixes. 6 | 7 | Report issues here thanks https://github.com/Osmodium/PathfinderTextToSpeechMod/issues -------------------------------------------------------------------------------- /Changelog/1.0.4.txt: -------------------------------------------------------------------------------- 1 | SpeechMod 2 | 3 | Version 1.0.4 Changelog 4 | 5 | - Support for version 1.0.3. 6 | - Added TTS for companion stories under summary and biography. 7 | - Various slight optimizations and stability fixes. 8 | 9 | Report issues here thanks https://github.com/Osmodium/PathfinderTextToSpeechMod/issues -------------------------------------------------------------------------------- /Changelog/1.1.1.txt: -------------------------------------------------------------------------------- 1 | SpeechMod 2 | 3 | Version 1.1.1 Changelog 4 | 5 | - Added loading screen TTS support 6 | - Added playback of dialog and book answers if enabled 7 | - Added hotkey to stop the playback (default ctrl+s) 8 | 9 | Report issues here thanks https://github.com/Osmodium/PathfinderTextToSpeechMod/issues -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Christian Schubert 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SpeechMod 2 | By [Osmodium](https://github.com/Osmodium) 3 | 4 | ## This mod is made for Pathfinder: Wrath of the Righteous and introduces TTS (TextToSpeech) in most places. 5 | Version: 1.1.2 6 | 7 | **Disclaimer: Works on Windows and macOS only** 8 | 9 | **Works with all languages as long as you have a voice in that language installed.** 10 | 11 | - [How to unlock more voices in Windows 10/11](https://www.ghacks.net/2018/08/11/unlock-all-windows-10-tts-voices-system-wide-to-get-more-of-them/) 12 | - [How to use Natural voices in Windows 10/11](https://www.nexusmods.com/warhammer40kroguetrader/articles/7) 13 | 14 | ### How to use natural voices: 15 | 16 | Install this application: [NaturalVoiceSAPIAdapter](https://github.com/gexgd0419/NaturalVoiceSAPIAdapter) 17 | 18 | Known issue: If the mod is not showing in the mod manager, it might be due to installing "All supported languages". Try uninstalling and installing using the setting "Follow user's preferred languages". 19 | 20 | Disclaimer: I do NOT intend to support issues related to the NaturalVoicesSAPIAdapter application. If it looks like it is an issue with the WindowsVoice dll, I'll have a look at it. 21 | 22 | ### How to install 23 | 24 | 1. Download and install Unity Mod Manager, make sure it is at least version 0.23.0 (I use 0.23.3) 25 | 2. Run Unity Mod Manger and set it up to find Pathfinder: Wrath of the Righteous (Second Adventure) 26 | 3. Download the SpeechMod-mod 27 | 4. Install the mod by dragging the zip file from step 3 into the Unity Mod Manager window under the Mods tab. Alternatively locate the zip file after clicking the "Install" button in Unity Mod Manager. 28 | 29 | *If running on OSX 64-bit you might need to use the *mono console.exe* command (see UMM documentation for further) 30 | 31 | ### Known issues / limitations 32 | 33 | *If you find issues or would like to request features, please use the issues tracker in GitHub [here](https://github.com/Osmodium/PathfinderTextToSpeechMod/issues)* 34 | 35 | #### Limitations: 36 | - No stopping of playback yet. 37 | 38 | #### Issues todo: 39 | - No support for chapter changes (although they seem to be narrated). 40 | 41 | ### How to use 42 | 43 | #### 1) Dialog 44 | When in dialog mode you can now press the play button next to the left image to listen to the current block of dialog. If autoplay is enabled, you don't have to push the playbutton. 45 | 46 | ![Playbutton for the current dialog](https://dashvoid.com/speechmod/wrath/playbutton_dialog.png) 47 | 48 | #### 2) Book text 49 | When inspecting a book (through *right-click->Info*) *hover* over the text and *left click*. 50 | 51 | ![Here the hover behaviour is set to underline the text, see the settings for more custumization](https://dashvoid.com/speechmod/wrath/booktext.png) 52 | 53 | #### 3) Item text 54 | When inspecting an item (through *right-click->Info*) *hover* over text (not all text is currently supported) and *left click*. 55 | 56 | ![Some of the texts are not supported yet. Try hovering different parts to see which are supported](https://dashvoid.com/speechmod/wrath/itemtext.png) 57 | 58 | #### 4) Journal Quest text 59 | In the journal, each of the bigger text blocks and important stuff can be played through the play button adjacent to the text. 60 | 61 | ![The most important parts of the journal text is supported.](https://dashvoid.com/speechmod/wrath/journaltext_0_9_5.png) 62 | 63 | #### 5) Encyclopedia text 64 | In the encyclopedia the text blocks (defined by Owlcat) can be played by pressing the play button adjacent to the text. 65 | 66 | ![All text parts in the encyclopedia is supported.](https://dashvoid.com/speechmod/wrath/encyclopediatext_0_9_5.png) 67 | 68 | #### 6) Book Event text 69 | When encountering a book event, the text can be played by hovering the text part (it will apply the chosen hover effect) and left-clicking. 70 | 71 | ![All text parts in a book event is supported. You might even get to know what the cut text says ;)](https://dashvoid.com/speechmod/wrath/eventbook_0_9_6.png) 72 | 73 | #### 7) Messagebox text 74 | The various pop-up boxes that eventually shows up throughout the game, can be played when hovered and left-clicked. 75 | 76 | ![Some texts might be so important that I decided to add support for them.](https://dashvoid.com/speechmod/wrath/messagemodal_0_9_6.png) 77 | 78 | 79 | #### 8) Combat Results text 80 | When your amy has defeated an enemy, the resulting message text is also supported for playback when hovered and left-clicked. 81 | 82 | ![Some of the combat results from armies might be important.](https://dashvoid.com/speechmod/wrath/combatresult_1_0_0.png) 83 | 84 | #### 9) Tutorial Windows text 85 | Both big and small tutorial windows text is supported and can be played by hovering and left-clicking. 86 | 87 | ![Tutorials can be helpful to learn more.](https://dashvoid.com/speechmod/wrath/tutorialsmall_1_0_0.png) 88 | 89 | #### 10) Character story under summary and biography 90 | When inspecting a character, the story of that character is displayed both under *Summary* and under *Biography*, and are both supported by hovering and left-clicking. 91 | 92 | ![Stories give companions depth.](https://dashvoid.com/speechmod/wrath/story_1_0_4.png) 93 | 94 | #### 11) Loading screen hints 95 | When loading the loading screen hint can be played back. 96 | 97 | ![Get some great advice from the loading screen.](https://dashvoid.com/speechmod/wrath/loading_message.png) 98 | 99 | ### Settings 100 | 101 | New keybind setting in the game menu under "Sound" to stop playback. 102 | ![Assign keybind(s) to stopping of playback](https://dashvoid.com/speechmod/wrath/keybind_stop_playback.png) 103 | 104 | If enabled in the mod-settings, a notification will be shown when stopping the playback through use of the keybind. 105 | 106 | ![Notification showing that the playback has stopped](https://dashvoid.com/speechmod/wrath/playback_stopped_info.png) 107 | 108 | The different settings (available through *ctrl+f10* if not overridden in the UMM) for SpeechMod 109 | - **Narrator Voice**: The settings for the voice used for either all or non-gender specific text in dialogs when *Use gender specific voices* is turned on. 110 | - *Nationality*: Just shows the selected voices nationality. 111 | - **Speech rate**: The speed of the voice the higher number, the faster the speech. 112 | - Windows: from -10 to 10 (relative speed from 0). 113 | - macOS: from 150 to 300 (words per minute). 114 | - Windows Only: 115 | - **Speech volume**: The volume of the voice from 0 to 100. 116 | - **Speech pitch**: The pitch of the voice from -10 to 10. 117 | -**Preview Voice**: Used to preview the settings of the voice. 118 | - **Use gender specific voices**: Specify voices for female and male dialog parts. Each of the voices can be adjusted with rate, volume and pitch where available. 119 | - Windows Only: 120 | - **Interrupt speech on play**: 2 settings: *Interrupt and play* or *Add to queue*, hope this speaks for itself. 121 | - **Auto play dialog**: When enabled, dialogs will be played automatically when theres no voice acted dialog. 122 | - **Auto play ignores voiced dialog lines**: Only available when using auto play dialog. This option makes the auto play ignore when there is voiced dialog, remember to turn dialog off in the settings. 123 | - **Show playback button of dialog answers**: Display a playback button next to the dialog answers, left-click it to play the dialog line. 124 | - **Include dialog answer number in playback**: When playing the dialog answer, include the respective number. 125 | - **Color answer on hover**: Colorizes the background of the dialog answer for clearer indication that it is not being chosen, but played back. 126 | - **Enable color on text hover**: This is used only for the text boxes when inspecting items, and colors the text the selected color when hovering the text box. 127 | - **Enable font style on hover**: As above this is only used for text boxes, but lets you set the style of the font. 128 | - **Phonetic Dictionary Reload**: Reloads the PhoneticDictionary.json into the game, to facilitate modificaton while playing. (Note that the keys are now regex enabled, so it might need an update if you use this) 129 | 130 | ![Settings for SpeechMod](https://dashvoid.com/speechmod/wrath/settings_1_1_1.png) 131 | 132 | ### Motivation 133 | *Why did I create this mod?* 134 | I have come to realize that I spend alot of my energy through the day on various activities, so when I get to play a game I rarely have enough energy left over to focus on reading long passages of text. So I thought it nice if I could get a helping hand so I wouldn't miss out on the excellent stories and writing in text heavy games. 135 | After I started creating this mod, I have thought to myself that if I struggle with this issue, imageine what people with genuine disabilities must go through and possibly miss out on, which motivated me even more to get this mod working and release it. I really hope that it will help and encourage more people to get as much out of the game as possible. 136 | 137 | ### Contribute 138 | If you find a name in the game which is pronounced funny by the voice, you can add it to the PhoneticDictionary.json in the mod folder (don't uninstall the mod as this will be deleted). I don't have a great way of submitting changes to this besides through GitHub pull requests, which is not super user friendly. But let's see if we can build a good pronounciation database for the voice together. 139 | Also feel free to hit me up with ideas, issues and PRs on GitHub or NexusMods :) 140 | 141 | ### Acknowledgments 142 | - [Chad Weisshaar](https://chadweisshaar.com/blog/author/wp_admin/) for his blog about [Windows TTS for Unity](https://chadweisshaar.com/blog/2015/07/02/microsoft-speech-for-unity/) 143 | - [dope0ne](https://forums.nexusmods.com/index.php?/user/895998-dope0ne/) (zer0bits) for providing code to support macOS, and various exploration work. 144 | - [gexgd0419](https://github.com/gexgd0419) for his work on the [NaturalVoiceSAPIAdapter](https://github.com/gexgd0419/NaturalVoiceSAPIAdapter) 145 | - Owlcat Discord channel members 146 | - Join the [Discord](https://discord.gg/EFWq7rJFNN) -------------------------------------------------------------------------------- /SpeechMod.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.32112.339 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpeechMod", "SpeechMod\SpeechMod.csproj", "{DE7D77D9-DB2B-456B-B2E8-734963C7DE0D}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Test", "Test\Test.csproj", "{A75D9932-76B4-44F0-9478-CAF648B0996E}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionItems", "{7D8E49C6-69D8-4A9E-91E0-663FDAF2F147}" 11 | ProjectSection(SolutionItems) = preProject 12 | README.md = README.md 13 | Todo.txt = Todo.txt 14 | EndProjectSection 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|Any CPU = Debug|Any CPU 19 | Release|Any CPU = Release|Any CPU 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {DE7D77D9-DB2B-456B-B2E8-734963C7DE0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {DE7D77D9-DB2B-456B-B2E8-734963C7DE0D}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {DE7D77D9-DB2B-456B-B2E8-734963C7DE0D}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {DE7D77D9-DB2B-456B-B2E8-734963C7DE0D}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {A75D9932-76B4-44F0-9478-CAF648B0996E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {A75D9932-76B4-44F0-9478-CAF648B0996E}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {A75D9932-76B4-44F0-9478-CAF648B0996E}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {A75D9932-76B4-44F0-9478-CAF648B0996E}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {65A6B4C7-22CA-4183-A92B-FFCCD3369DA3} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /SpeechMod/Configuration/ModConfigurationManager.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Kingmaker; 3 | using Kingmaker.Settings; 4 | using Kingmaker.UI; 5 | using Kingmaker.UI.SettingsUI; 6 | using SpeechMod.Configuration.Settings; 7 | using SpeechMod.Configuration.UI; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | using static UnityModManagerNet.UnityModManager; 12 | 13 | namespace SpeechMod.Configuration; 14 | 15 | public class ModConfigurationManager 16 | { 17 | public Dictionary> GroupedSettings = new(); 18 | public Harmony HarmonyInstance { get; protected set; } 19 | public ModEntry ModEntry { get; protected set; } 20 | public string SettingsPrefix = Guid.NewGuid().ToString(); 21 | 22 | private ModConfigurationManager() { } 23 | 24 | public static void Build(Harmony harmonyInstance, ModEntry modEntry, string settingsPrefix) 25 | { 26 | Instance.HarmonyInstance = harmonyInstance; 27 | Instance.ModEntry = modEntry; 28 | Instance.SettingsPrefix = settingsPrefix; 29 | } 30 | 31 | private bool Initialized = false; 32 | 33 | public void Initialize() 34 | { 35 | if (Initialized) return; 36 | Initialized = true; 37 | 38 | foreach (var setting in GroupedSettings.SelectMany(settings => settings.Value)) 39 | { 40 | setting.BuildUIAndLink(); 41 | setting.TryEnable(); 42 | } 43 | 44 | if (ModHotkeySettingEntry.ReSavingRequired) 45 | { 46 | SettingsController.SaveAll(); 47 | Instance.ModEntry.Logger.Log("Hotkey settings were migrated"); 48 | } 49 | } 50 | 51 | public static ModConfigurationManager Instance { get; } = new(); 52 | } 53 | 54 | [HarmonyPatch] 55 | public static class SettingsUIPatches 56 | { 57 | [HarmonyPatch(typeof(UISettingsManager), nameof(UISettingsManager.Initialize))] 58 | [HarmonyPostfix] 59 | static void AddSettingsGroup() 60 | { 61 | if (Game.Instance.UISettingsManager.m_SoundSettingsList.Any(group => group.name?.StartsWith(ModConfigurationManager.Instance.SettingsPrefix) ?? false)) 62 | { 63 | return; 64 | } 65 | 66 | ModConfigurationManager.Instance?.Initialize(); 67 | 68 | foreach (var settings in ModConfigurationManager.Instance.GroupedSettings) 69 | { 70 | Game.Instance.UISettingsManager.m_SoundSettingsList?.Add( 71 | OwlcatUITools.MakeSettingsGroup($"{ModConfigurationManager.Instance.SettingsPrefix}.group.{settings.Key}", "Speech Mod", 72 | settings.Value?.Select(x => x.GetUISettings()).ToArray() 73 | )); 74 | } 75 | } 76 | 77 | [HarmonyPatch(typeof(KeyboardAccess), nameof(KeyboardAccess.CanBeRegistered))] 78 | [HarmonyPrefix] 79 | public static bool CanRegisterAnything(ref bool __result, string name) 80 | { 81 | if (name == null || !name.StartsWith(ModConfigurationManager.Instance.SettingsPrefix)) 82 | { 83 | return true; 84 | } 85 | __result = true; 86 | return false; 87 | } 88 | } -------------------------------------------------------------------------------- /SpeechMod/Configuration/Settings/ModHotkeySettingEntry.cs: -------------------------------------------------------------------------------- 1 | using Kingmaker; 2 | using Kingmaker.Settings; 3 | using Kingmaker.UI; 4 | using Kingmaker.UI.SettingsUI; 5 | using SpeechMod.Localization; 6 | using System; 7 | using UnityEngine; 8 | 9 | namespace SpeechMod.Configuration.Settings; 10 | 11 | public abstract class ModHotkeySettingEntry : ModSettingEntry 12 | { 13 | public readonly SettingsEntityKeyBindingPair SettingEntity; 14 | public UISettingsEntityKeyBinding UiSettingEntity { get; private set; } 15 | 16 | public static bool ReSavingRequired { get; private set; } = false; 17 | 18 | protected ModHotkeySettingEntry(string key, string title, string tooltip, string defaultKeyPairString) : base(key, title, tooltip) 19 | { 20 | try 21 | { 22 | SettingEntity = new SettingsEntityKeyBindingPair($"{ModConfigurationManager.Instance?.SettingsPrefix}.newcontrols.{Key}", new(defaultKeyPairString, KeyboardAccess.GameModesGroup.All), false, true); 23 | } 24 | catch (Exception ex) 25 | { 26 | ModConfigurationManager.Instance?.ModEntry?.Logger?.Error($"Failed to create {Key} setting entity: {ex}"); 27 | } 28 | } 29 | 30 | public override UISettingsEntityBase GetUISettings() => UiSettingEntity; 31 | 32 | public string GetBindName() => $"{ModConfigurationManager.Instance?.SettingsPrefix}.newcontrols.ui.{Key}"; 33 | public override void BuildUIAndLink() 34 | { 35 | UiSettingEntity = MakeKeyBind(); 36 | UiSettingEntity.LinkSetting(SettingEntity); 37 | (SettingEntity as IReadOnlySettingEntity).OnValueChanged += delegate 38 | { 39 | TryEnable(); 40 | }; 41 | } 42 | 43 | private UISettingsEntityKeyBinding MakeKeyBind() 44 | { 45 | var keyBindSetting = ScriptableObject.CreateInstance(); 46 | keyBindSetting.m_Description = ModLocalizationManager.CreateString($"{ModConfigurationManager.Instance?.SettingsPrefix}.feature.{Key}.description", Title); 47 | keyBindSetting.m_TooltipDescription = ModLocalizationManager.CreateString($"{ModConfigurationManager.Instance?.SettingsPrefix}.feature.{Key}.tooltip-description", Tooltip); 48 | keyBindSetting.name = $"{ModConfigurationManager.Instance?.SettingsPrefix}.newcontrols.ui.{Key}"; 49 | return keyBindSetting; 50 | } 51 | 52 | protected void RegisterKeybind() 53 | { 54 | if (Status != SettingStatus.NotApplied) return; 55 | 56 | var currentValue = SettingEntity.GetValue(); 57 | 58 | if (currentValue.Binding1.Key != KeyCode.None) 59 | { 60 | Game.Instance.Keyboard.RegisterBinding( 61 | GetBindName(), 62 | currentValue.Binding1, 63 | currentValue.GameModesGroup, 64 | currentValue.TriggerOnHold); 65 | ModConfigurationManager.Instance?.ModEntry?.Logger?.Log($"{Title} binding 1 registered: {currentValue.Binding1}"); 66 | } 67 | else 68 | { 69 | ModConfigurationManager.Instance?.ModEntry?.Logger?.Log($"{Title} binding 1 empty"); 70 | } 71 | 72 | if (currentValue.Binding2.Key != KeyCode.None) 73 | { 74 | Game.Instance.Keyboard.RegisterBinding( 75 | GetBindName(), 76 | currentValue.Binding2, 77 | currentValue.GameModesGroup, 78 | currentValue.TriggerOnHold); 79 | ModConfigurationManager.Instance?.ModEntry?.Logger?.Log($"{Title} binding 2 registered: {currentValue.Binding2}"); 80 | } 81 | else 82 | { 83 | ModConfigurationManager.Instance?.ModEntry?.Logger?.Log($"{Title} binding 2 empty"); 84 | } 85 | } 86 | 87 | protected SettingStatus TryEnableAndPatch(Type type) 88 | { 89 | TryFix(); 90 | if (Status != SettingStatus.NotApplied) 91 | { 92 | return Status; 93 | } 94 | 95 | RegisterKeybind(); 96 | var currentValue = SettingEntity.GetValue(); 97 | if (currentValue.Binding1.Key != KeyCode.None || currentValue.Binding2.Key != KeyCode.None) 98 | { 99 | return TryPatchInternal(type); 100 | } 101 | else 102 | { 103 | ModConfigurationManager.Instance?.ModEntry?.Logger?.Log($"{Title} binding 1 and binding 2 empty, setting integration skipped"); 104 | } 105 | return SettingStatus.NotApplied; 106 | } 107 | 108 | /// 109 | /// If hotkey group or trigger changes, those values need to be updated manually 110 | /// and later saved 111 | /// 112 | private void TryFix() 113 | { 114 | var curValue = SettingEntity.GetValue(); 115 | var defaultGroup = SettingEntity.DefaultValue.GameModesGroup; 116 | var defaultTrigger = SettingEntity.DefaultValue.TriggerOnHold; 117 | if (curValue.GameModesGroup == defaultGroup && curValue.TriggerOnHold == defaultTrigger) 118 | return; 119 | curValue.GameModesGroup = defaultGroup; 120 | curValue.TriggerOnHold = defaultTrigger; 121 | SettingEntity.SetValueAndConfirm(curValue); 122 | ReSavingRequired = true; 123 | ModConfigurationManager.Instance?.ModEntry?.Logger?.Log($"{Title} had outdated hotkey settings, migrated."); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /SpeechMod/Configuration/Settings/ModSettingEntry.cs: -------------------------------------------------------------------------------- 1 | using Kingmaker.UI.SettingsUI; 2 | using System; 3 | 4 | namespace SpeechMod.Configuration.Settings; 5 | 6 | public abstract class ModSettingEntry 7 | { 8 | public readonly string Key; 9 | public readonly string Title; 10 | public readonly string Tooltip; 11 | 12 | public SettingStatus Status { get; private set; } = SettingStatus.NotApplied; 13 | 14 | protected ModSettingEntry(string key, string title, string tooltip) 15 | { 16 | Key = key; 17 | Title = title; 18 | Tooltip = tooltip; 19 | } 20 | 21 | public abstract SettingStatus TryEnable(); 22 | 23 | public abstract void BuildUIAndLink(); 24 | 25 | public abstract UISettingsEntityBase GetUISettings(); 26 | 27 | protected SettingStatus TryPatchInternal(params Type[] type) 28 | { 29 | if (Status != SettingStatus.NotApplied) return Status; 30 | try 31 | { 32 | foreach (var t in type) 33 | { 34 | ModConfigurationManager.Instance?.HarmonyInstance?.CreateClassProcessor(t)?.Patch(); 35 | } 36 | Status = SettingStatus.Working; 37 | ModConfigurationManager.Instance?.ModEntry?.Logger?.Log($"{Title} patch succeeded"); 38 | } 39 | catch (Exception ex) 40 | { 41 | ModConfigurationManager.Instance?.ModEntry?.Logger?.Error($"{Title} patch exception: {ex.Message}"); 42 | Status = SettingStatus.Error; 43 | } 44 | return Status; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /SpeechMod/Configuration/Settings/ModToggleSettingEntry.cs: -------------------------------------------------------------------------------- 1 | using Kingmaker.Settings; 2 | using Kingmaker.UI.SettingsUI; 3 | using SpeechMod.Localization; 4 | using System; 5 | using UnityEngine; 6 | 7 | namespace SpeechMod.Configuration.Settings; 8 | 9 | public abstract class ModToggleSettingEntry : ModSettingEntry 10 | { 11 | public readonly SettingsEntityBool SettingEntity; 12 | public UISettingsEntityBool UiSettingEntity { get; private set; } 13 | 14 | protected ModToggleSettingEntry(string key, string title, string tooltip, bool defaultValue) : base(key, title, tooltip) 15 | { 16 | SettingEntity = new($"{ModConfigurationManager.Instance?.SettingsPrefix}.newcontrols.{Key}", defaultValue, false, true); 17 | } 18 | 19 | public override UISettingsEntityBase GetUISettings() => UiSettingEntity; 20 | 21 | public override void BuildUIAndLink() 22 | { 23 | UiSettingEntity = ScriptableObject.CreateInstance(); 24 | UiSettingEntity.m_Description = ModLocalizationManager.CreateString($"{ModConfigurationManager.Instance?.SettingsPrefix}.feature.{Key}.description", Title); 25 | UiSettingEntity.m_TooltipDescription = ModLocalizationManager.CreateString($"{ModConfigurationManager.Instance?.SettingsPrefix}.feature.{Key}.tooltip-description", Tooltip); 26 | UiSettingEntity.DefaultValue = false; 27 | UiSettingEntity.LinkSetting(SettingEntity); 28 | (SettingEntity as IReadOnlySettingEntity).OnValueChanged += delegate 29 | { 30 | TryEnable(); 31 | }; 32 | } 33 | 34 | protected SettingStatus TryEnableAndPatch(Type type) 35 | { 36 | var currentValue = SettingEntity.GetValue(); 37 | if (currentValue) 38 | { 39 | return TryPatchInternal(type); 40 | } 41 | else 42 | { 43 | ModConfigurationManager.Instance?.ModEntry?.Logger?.Log($"{Title} disabled, setting integration skipped"); 44 | } 45 | return SettingStatus.NotApplied; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /SpeechMod/Configuration/Settings/SettingStatus.cs: -------------------------------------------------------------------------------- 1 | namespace SpeechMod.Configuration.Settings; 2 | 3 | public enum SettingStatus 4 | { 5 | NotApplied = 0, 6 | Working = 1, 7 | Error = 2 8 | } 9 | -------------------------------------------------------------------------------- /SpeechMod/Configuration/UI/OwlcatUITools.cs: -------------------------------------------------------------------------------- 1 | using Kingmaker.UI.SettingsUI; 2 | using SpeechMod.Localization; 3 | using UnityEngine; 4 | 5 | namespace SpeechMod.Configuration.UI; 6 | 7 | public static class OwlcatUITools 8 | { 9 | public static UISettingsGroup MakeSettingsGroup(string key, string name, params UISettingsEntityBase[] settings) 10 | { 11 | var group = ScriptableObject.CreateInstance(); 12 | group.name = key; 13 | group.Title = ModLocalizationManager.CreateString(key, name); 14 | 15 | group.SettingsList = settings; 16 | 17 | return group; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /SpeechMod/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace SpeechMod; 2 | 3 | public static class Constants 4 | { 5 | public const string WINDOWS_VOICE_DLL = "WindowsVoice"; 6 | public const string WINDOWS_VOICE_NAME = "WindowsVoice"; 7 | public const string APPLE_VOICE_NAME = "AppleVoice"; 8 | public const string SETTINGS_PREFIX = "osmodium.speechmod"; 9 | } 10 | -------------------------------------------------------------------------------- /SpeechMod/Info.json: -------------------------------------------------------------------------------- 1 | { 2 | "Id": "SpeechMod", 3 | "DisplayName": "SpeechMod for Pathfinder: Wrath of the Righteous", 4 | "Author": "Osmodium", 5 | "ManagerVersion": "0.31.1", 6 | "Requirements": [], 7 | "AssemblyName": "PathfinderWOTRSpeechMod.dll", 8 | "EntryMethod": "SpeechMod.Main.Load", 9 | "GameVersion": "2.6.0n", 10 | "HomePage": "https://www.nexusmods.com/pathfinderwrathoftherighteous/mods/241", 11 | "Version": "1.1.2" 12 | } -------------------------------------------------------------------------------- /SpeechMod/Keybinds/PlaybackStop.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Kingmaker; 3 | using Kingmaker.Localization; 4 | using Kingmaker.UI.MVVM._PCView.Common; 5 | using SpeechMod.Configuration.Settings; 6 | #if DEBUG 7 | using UnityEngine; 8 | #endif 9 | 10 | namespace SpeechMod.Keybinds; 11 | 12 | public class PlaybackStop : ModHotkeySettingEntry 13 | { 14 | private const string _key = "playback.stop"; 15 | private const string _title = "Stop playback"; 16 | private const string _tooltip = "Stops playback of SpeechMod TTS."; 17 | private const string _defaultValue = "%S;;All;false"; 18 | private const string BIND_NAME = $"{Constants.SETTINGS_PREFIX}.newcontrols.ui.{_key}"; 19 | 20 | public PlaybackStop() : base(_key, _title, _tooltip, _defaultValue) 21 | { } 22 | 23 | public override SettingStatus TryEnable() => TryEnableAndPatch(typeof(Patches)); 24 | 25 | [HarmonyPatch] 26 | private static class Patches 27 | { 28 | [HarmonyPatch(typeof(CommonPCView), nameof(CommonPCView.BindViewImplementation))] 29 | [HarmonyPostfix] 30 | private static void Add(CommonPCView __instance) 31 | { 32 | #if DEBUG 33 | Debug.Log($"{nameof(CommonPCView)}_{nameof(CommonPCView.BindViewImplementation)}_Postfix : {BIND_NAME}"); 34 | #endif 35 | __instance?.AddDisposable(Game.Instance!.Keyboard!.Bind(BIND_NAME, delegate { StopPlayback(__instance); })); 36 | } 37 | 38 | private static void StopPlayback(CommonPCView instance) 39 | { 40 | if (!Main.Speech?.IsSpeaking() == true) 41 | return; 42 | 43 | if (instance.m_WarningsText != null) 44 | { 45 | var text = LocalizationManager.CurrentPack!.GetText("osmodium.speechmod.feature.playback.stop.notification", false); 46 | 47 | if (string.IsNullOrEmpty(text)) 48 | text = "SpeechMod: Playback stopped!"; 49 | 50 | if (Main.Settings!.ShowNotificationOnPlaybackStop) 51 | instance.m_WarningsText?.Show(text); 52 | } 53 | 54 | Main.Speech?.Stop(); 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /SpeechMod/Localization/ModLocalizationManager.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Kingmaker.Localization; 3 | using Kingmaker.Localization.Shared; 4 | using Newtonsoft.Json; 5 | using SpeechMod.Configuration; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | 9 | namespace SpeechMod.Localization; 10 | 11 | [HarmonyPatch] 12 | public static class ModLocalizationManager 13 | { 14 | private static ModLocalizationPack m_EnPack; 15 | 16 | [HarmonyPatch(typeof(LocalizationManager), nameof(LocalizationManager.Init))] 17 | [HarmonyPostfix] 18 | public static void Init() 19 | { 20 | m_EnPack = LoadPack(Locale.enGB); 21 | 22 | //ApplyLocalization(); 23 | } 24 | 25 | [HarmonyPatch(typeof(LocalizationManager), nameof(LocalizationManager.OnLocaleChanged))] 26 | [HarmonyPostfix] 27 | public static void ApplyLocalization() 28 | { 29 | var currentPack = LocalizationManager.CurrentPack; 30 | if (currentPack == null) 31 | { 32 | return; 33 | } 34 | 35 | foreach (var entry in m_EnPack.Strings) 36 | { 37 | currentPack.PutString(entry.Key, entry.Value.Text); 38 | } 39 | 40 | if (LocalizationManager.CurrentLocale != Locale.enGB) 41 | { 42 | var localized = LoadPack(LocalizationManager.CurrentLocale); 43 | foreach (var entry in localized.Strings) 44 | { 45 | currentPack.PutString(entry.Key, entry.Value.Text); 46 | } 47 | } 48 | 49 | #if DEBUG 50 | var localizationFolder = Path.Combine(ModConfigurationManager.Instance?.ModEntry?.Path!, "Localization"); 51 | var packFile = Path.Combine(localizationFolder, Locale.enGB + ".json"); 52 | using StreamWriter file = new(packFile); 53 | using JsonWriter jsonReader = new JsonTextWriter(file); 54 | JsonSerializer serializer = new(); 55 | serializer.Serialize(jsonReader, m_EnPack); 56 | #endif 57 | } 58 | 59 | private static ModLocalizationPack LoadPack(Locale locale) 60 | { 61 | var localizationFolder = Path.Combine(ModConfigurationManager.Instance?.ModEntry?.Path!, "Localization"); 62 | var packFile = Path.Combine(localizationFolder, locale + ".json"); 63 | if (File.Exists(packFile)) 64 | { 65 | try 66 | { 67 | using var file = File.OpenText(packFile); 68 | using JsonReader jsonReader = new JsonTextReader(file); 69 | JsonSerializer serializer = new(); 70 | var enLocalization = serializer.Deserialize(jsonReader); 71 | return enLocalization; 72 | } 73 | catch (System.Exception ex) 74 | { 75 | ModConfigurationManager.Instance?.ModEntry?.Logger?.Error($"Failed to read or parse {locale} mod localization pack: {ex.Message}"); 76 | } 77 | } 78 | else 79 | { 80 | ModConfigurationManager.Instance?.ModEntry?.Logger?.Log($"Missing localization pack for {locale}"); 81 | } 82 | return new() { Strings = new() }; 83 | } 84 | 85 | public static LocalizedString CreateString(string key, string value) 86 | { 87 | if (m_EnPack.Strings.ContainsKey(key)) 88 | { 89 | return new LocalizedString { m_ShouldProcess = false, m_Key = key }; 90 | } 91 | 92 | ModConfigurationManager.Instance?.ModEntry?.Logger?.Log($"Missing localization string {key}"); 93 | #if DEBUG 94 | m_EnPack.Strings[key] = new() { Text = value }; 95 | #endif 96 | return new LocalizedString { m_ShouldProcess = false, m_Key = key }; 97 | } 98 | } 99 | 100 | public record ModLocalizationPack 101 | { 102 | [JsonProperty] 103 | public Dictionary Strings; 104 | } 105 | 106 | public struct ModLocalizationEntry 107 | { 108 | [JsonProperty] 109 | public string Text; 110 | }; -------------------------------------------------------------------------------- /SpeechMod/Localization/enGB.json: -------------------------------------------------------------------------------- 1 | { 2 | "Strings": { 3 | "osmodium.speechmod.group.main": { 4 | "Text": "Speech Mod" 5 | }, 6 | "osmodium.speechmod.feature.playback.stop.description": { 7 | "Text": "Stop playback" 8 | }, 9 | "osmodium.speechmod.feature.playback.stop.tooltip-description": { 10 | "Text": "Stops playback of SpeechMod TTS when it is playing." 11 | }, 12 | "osmodium.speechmod.feature.playback.stop.notification": { 13 | "Text": "SpeechMod: Playback stopped!" 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /SpeechMod/Main.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using SpeechMod.Configuration; 3 | using SpeechMod.Keybinds; 4 | using SpeechMod.Unity; 5 | using SpeechMod.Voice; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Reflection; 10 | using TMPro; 11 | using UnityEngine; 12 | using UnityModManagerNet; 13 | 14 | namespace SpeechMod; 15 | 16 | #if DEBUG 17 | [EnableReloading] 18 | #endif 19 | public static class Main 20 | { 21 | public static UnityModManager.ModEntry.ModLogger Logger; 22 | public static Settings Settings; 23 | public static bool Enabled; 24 | 25 | public static string[] FontStyleNames = Enum.GetNames(typeof(FontStyles)); 26 | 27 | public static string NarratorVoice => VoicesDict?.ElementAtOrDefault(Settings.NarratorVoice).Key; 28 | public static string FemaleVoice => VoicesDict?.ElementAtOrDefault(Settings.FemaleVoice).Key; 29 | public static string MaleVoice => VoicesDict?.ElementAtOrDefault(Settings.MaleVoice).Key; 30 | 31 | public static Dictionary VoicesDict => Settings?.AvailableVoices?.Select(v => 32 | { 33 | var splitV = v?.Split('#'); 34 | return splitV.Length != 2 35 | ? new { Key = v, Value = "Unknown" } 36 | : new { Key = splitV[0], Value = splitV[1] }; 37 | }).ToDictionary(p => p.Key, p => p.Value); 38 | 39 | public static ISpeech Speech; 40 | private static bool m_Loaded = false; 41 | 42 | private static bool Load(UnityModManager.ModEntry modEntry) 43 | { 44 | Debug.Log("Pathfinder: Wrath of the Righteous Speech Mod Initializing..."); 45 | 46 | Logger = modEntry.Logger; 47 | 48 | if (!SetSpeech()) 49 | return false; 50 | 51 | Settings = UnityModManager.ModSettings.Load(modEntry); 52 | MenuGUI.UpdateColors(); 53 | 54 | modEntry.OnToggle = OnToggle; 55 | modEntry.OnGUI = OnGui; 56 | modEntry.OnSaveGUI = OnSaveGui; 57 | 58 | var harmony = new Harmony(modEntry.Info.Id); 59 | harmony.PatchAll(Assembly.GetExecutingAssembly()); 60 | 61 | ModConfigurationManager.Build(harmony, modEntry, Constants.SETTINGS_PREFIX); 62 | SetUpSettings(); 63 | harmony.CreateClassProcessor(typeof(SettingsUIPatches)).Patch(); 64 | 65 | Logger.Log(Speech.GetStatusMessage()); 66 | 67 | if (!SetAvailableVoices()) 68 | return false; 69 | 70 | PhoneticDictionary.LoadDictionary(); 71 | 72 | Debug.Log("Pathfinder: Wrath of the Righteous Speech Mod Initialized!"); 73 | m_Loaded = true; 74 | return true; 75 | } 76 | 77 | private static void SetUpSettings() 78 | { 79 | if (ModConfigurationManager.Instance.GroupedSettings.TryGetValue("main", out _)) 80 | return; 81 | 82 | ModConfigurationManager.Instance.GroupedSettings.Add("main", [new PlaybackStop()]); 83 | } 84 | 85 | private static bool SetAvailableVoices() 86 | { 87 | var availableVoices = Speech?.GetAvailableVoices(); 88 | 89 | if (availableVoices == null || availableVoices.Length == 0) 90 | { 91 | Logger.Warning("No available voices found! Disabling mod!"); 92 | return false; 93 | } 94 | 95 | //#if DEBUG 96 | Logger.Log("Available voices:"); 97 | foreach (var voice in availableVoices) 98 | { 99 | Logger.Log(voice); 100 | } 101 | //#endif 102 | Logger.Log("Setting available voices list..."); 103 | 104 | for (var i = 0; i < availableVoices.Length; i++) 105 | { 106 | var splitVoice = availableVoices[i].Split('#'); 107 | if (splitVoice.Length != 2 || string.IsNullOrEmpty(splitVoice[1])) 108 | availableVoices[i] = availableVoices[i].Replace("#","").Trim() + "#Unknown"; 109 | } 110 | 111 | // Ensure that the selected voice index falls within the available voices range 112 | if (Settings.NarratorVoice >= availableVoices.Length) 113 | { 114 | Logger.Log($"{nameof(Settings.NarratorVoice)} was out of range, resetting to first voice available."); 115 | Settings.NarratorVoice = 0; 116 | } 117 | 118 | if (Settings.FemaleVoice >= availableVoices.Length) 119 | { 120 | Logger.Log($"{nameof(Settings.FemaleVoice)} was out of range, resetting to first voice available."); 121 | Settings.FemaleVoice = 0; 122 | } 123 | 124 | if (Settings.MaleVoice >= availableVoices.Length) 125 | { 126 | Logger.Log($"{nameof(Settings.MaleVoice)} was out of range, resetting to first voice available."); 127 | Settings.MaleVoice = 0; 128 | } 129 | 130 | Settings.AvailableVoices = availableVoices.OrderBy(v => v.Split('#').ElementAtOrDefault(1)).ToArray(); 131 | 132 | return true; 133 | } 134 | 135 | private static bool SetSpeech() 136 | { 137 | switch (Application.platform) 138 | { 139 | case RuntimePlatform.OSXPlayer: 140 | Speech = new AppleSpeech(); 141 | SpeechExtensions.AddUiElements(Constants.APPLE_VOICE_NAME); 142 | break; 143 | case RuntimePlatform.WindowsPlayer: 144 | Speech = new WindowsSpeech(); 145 | SpeechExtensions.AddUiElements(Constants.WINDOWS_VOICE_NAME); 146 | break; 147 | default: 148 | Logger.Critical($"SpeechMod is not supported on {Application.platform}!"); 149 | return false; 150 | } 151 | 152 | return true; 153 | } 154 | 155 | private static bool OnToggle(UnityModManager.ModEntry modEntry, bool value) 156 | { 157 | Enabled = value; 158 | return true; 159 | } 160 | 161 | private static void OnGui(UnityModManager.ModEntry modEntry) 162 | { 163 | if (m_Loaded) 164 | MenuGUI.OnGui(); 165 | } 166 | 167 | private static void OnSaveGui(UnityModManager.ModEntry modEntry) 168 | { 169 | MenuGUI.UpdateColors(); 170 | Settings.Save(modEntry); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /SpeechMod/Patches/BookEventView_Patch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Kingmaker.UI.MVVM._PCView.Dialog.BookEvent; 3 | using Kingmaker.UI.MVVM._PCView.Dialog.Dialog; 4 | using SpeechMod.Unity.Extensions; 5 | using UnityEngine; 6 | 7 | #if DEBUG 8 | using Kingmaker; 9 | #endif 10 | 11 | namespace SpeechMod.Patches; 12 | 13 | [HarmonyPatch(typeof(BookEventView), "SetCues")] 14 | public static class BookEventView_Patch 15 | { 16 | private const string CANVAS_CUES_BLOCK_PATH = "NestedCanvas1/BookEventPCView/ContentWrapper/Window/Content/CuesBlock"; 17 | private const string GLOBALMAP_CUES_BLOCK_PATH = "BookEventView/ContentWrapper/Window/Content/CuesBlock"; 18 | 19 | public static void Postfix() 20 | { 21 | if (!Main.Enabled) 22 | return; 23 | 24 | #if DEBUG 25 | var sceneName = Game.Instance.CurrentlyLoadedArea.ActiveUIScene.SceneName; 26 | Debug.Log($"{nameof(BookEventView)}_SetCues_Postfix @ {sceneName}"); 27 | #endif 28 | 29 | var cuesBlock = UIHelper.TryFindInStaticCanvas(CANVAS_CUES_BLOCK_PATH, GLOBALMAP_CUES_BLOCK_PATH); 30 | 31 | if (cuesBlock == null) 32 | { 33 | Debug.LogWarning("CuesBlock not found!"); 34 | return; 35 | } 36 | 37 | cuesBlock.HookupTextToSpeechOnTransform(); 38 | } 39 | } -------------------------------------------------------------------------------- /SpeechMod/Patches/CharInfoAlignmentHistoryRecordView_Patch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Kingmaker.UI.MVVM._PCView.ServiceWindows.CharacterInfo.Sections.Alignment.AlignmentHistory; 3 | using SpeechMod.Unity.Extensions; 4 | #if DEBUG 5 | using UnityEngine; 6 | #endif 7 | 8 | namespace SpeechMod.Patches; 9 | 10 | [HarmonyPatch] 11 | public static class CharInfoAlignmentHistoryRecordView_Patch 12 | { 13 | [HarmonyPatch(typeof(CharInfoAlignmentHistoryRecordView), nameof(CharInfoAlignmentHistoryRecordView.BindViewImplementation))] 14 | [HarmonyPostfix] 15 | public static void BindViewImplementation_Postfix(CharInfoAlignmentHistoryRecordView __instance) 16 | { 17 | if (!Main.Enabled) 18 | return; 19 | 20 | #if DEBUG 21 | Debug.Log($"{nameof(CharInfoAlignmentHistoryRecordView)}_{nameof(BindViewImplementation_Postfix)}"); 22 | #endif 23 | 24 | __instance.m_Description.HookupTextToSpeech(); 25 | __instance.m_Shift.HookupTextToSpeech(); 26 | } 27 | } -------------------------------------------------------------------------------- /SpeechMod/Patches/CharInfoAlignmentWheelPCView_Patch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Kingmaker.UI.MVVM._PCView.ServiceWindows.CharacterInfo.Sections.Alignment.AlignmentWheel; 3 | using SpeechMod.Unity.Extensions; 4 | #if DEBUG 5 | using UnityEngine; 6 | #endif 7 | 8 | namespace SpeechMod.Patches; 9 | 10 | [HarmonyPatch] 11 | public static class CharInfoAlignmentWheelPCView_Patch 12 | { 13 | [HarmonyPatch(typeof(CharInfoAlignmentWheelPCView), nameof(CharInfoAlignmentWheelPCView.BindViewImplementation))] 14 | [HarmonyPostfix] 15 | public static void BindViewImplementation_Postfix(CharInfoAlignmentWheelPCView __instance) 16 | { 17 | if (!Main.Enabled) 18 | return; 19 | 20 | #if DEBUG 21 | Debug.Log($"{nameof(CharInfoAlignmentWheelPCView)}_{nameof(BindViewImplementation_Postfix)}"); 22 | #endif 23 | 24 | __instance.m_MythicLevel.HookupTextToSpeech(); 25 | } 26 | } -------------------------------------------------------------------------------- /SpeechMod/Patches/CharInfoCompanionStoryFullView_Patch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Kingmaker.UI.MVVM._PCView.ServiceWindows.CharacterInfo.Sections.Stories; 3 | using SpeechMod.Unity.Extensions; 4 | #if DEBUG 5 | using UnityEngine; 6 | #endif 7 | 8 | namespace SpeechMod.Patches; 9 | 10 | [HarmonyPatch] 11 | public static class CharInfoCompanionStoryFullView_Patch 12 | { 13 | [HarmonyPatch(typeof(CharInfoCompanionStoryFullView), nameof(CharInfoCompanionStoryFullView.BindViewImplementation))] 14 | [HarmonyPostfix] 15 | public static void BindViewImplementation_Postfix(CharInfoCompanionStoryFullView __instance) 16 | { 17 | if (!Main.Enabled) 18 | return; 19 | 20 | #if DEBUG 21 | Debug.Log($"{nameof(CharInfoCompanionStoryFullView)}_{nameof(BindViewImplementation_Postfix)}"); 22 | #endif 23 | 24 | __instance.m_Title.HookupTextToSpeech(); 25 | __instance.m_Description.HookupTextToSpeech(); 26 | } 27 | } -------------------------------------------------------------------------------- /SpeechMod/Patches/CombatResultView_Patch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Kingmaker.UI.MVVM._PCView.Crusade.CombatResult; 3 | using SpeechMod.Unity.Extensions; 4 | using UnityEngine; 5 | 6 | #if DEBUG 7 | using Kingmaker; 8 | #endif 9 | 10 | namespace SpeechMod.Patches; 11 | 12 | [HarmonyPatch(typeof(CombatResultPCView), "BindViewImplementation")] 13 | public static class CombatResultView_Patch 14 | { 15 | public static void Postfix() 16 | { 17 | if (!Main.Enabled) 18 | return; 19 | 20 | #if DEBUG 21 | var sceneName = Game.Instance.CurrentlyLoadedArea.ActiveUIScene.SceneName; 22 | Debug.Log($"{nameof(CombatResultPCView)}_BindViewImplementation_Postfix @ {sceneName}"); 23 | #endif 24 | 25 | var description = UIHelper.TryFindInStaticCanvas("CombatResultView/FoundDescription"); 26 | if (description == null) 27 | Debug.LogWarning($"{nameof(description)} not found!"); 28 | else 29 | description.HookupTextToSpeechOnTransform(); 30 | 31 | var experience = UIHelper.TryFindInStaticCanvas("CombatResultView/Experience"); 32 | if (experience == null) 33 | Debug.LogWarning($"{nameof(experience)} not found!"); 34 | else 35 | experience.HookupTextToSpeechOnTransform(); 36 | 37 | var resource = UIHelper.TryFindInStaticCanvas("CombatResultView/Resource"); 38 | if (resource == null) 39 | Debug.LogWarning($"{nameof(resource)} not found!"); 40 | else 41 | resource.HookupTextToSpeechOnTransform(); 42 | } 43 | } -------------------------------------------------------------------------------- /SpeechMod/Patches/DialogAnswerView_Patch.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using HarmonyLib; 3 | using Kingmaker; 4 | using Kingmaker.Blueprints; 5 | using Kingmaker.UI.MVVM._PCView.Dialog.Dialog; 6 | using SpeechMod.Unity; 7 | using SpeechMod.Unity.Extensions; 8 | using SpeechMod.Voice; 9 | using System.Text.RegularExpressions; 10 | using Owlcat.Runtime.UI.Controls.Button; 11 | using UnityEngine; 12 | using UnityEngine.UI; 13 | 14 | namespace SpeechMod.Patches; 15 | 16 | [HarmonyPatch] 17 | public static class DialogAnswerView_Patch 18 | { 19 | private const string DIALOG_ANSWER_ARROW_TEXTURE_PATH = "Arrow"; 20 | 21 | [HarmonyPatch(typeof(DialogAnswerView), nameof(DialogAnswerView.BindViewImplementation))] 22 | [HarmonyPostfix] 23 | public static void BindViewImplementation_Postfix(DialogAnswerView __instance) 24 | { 25 | if (!Main.Enabled) 26 | return; 27 | 28 | #if DEBUG 29 | Debug.Log($"{nameof(DialogAnswerView)}_{nameof(BindViewImplementation_Postfix)}"); 30 | #endif 31 | 32 | TryAddDialogButton(__instance); 33 | } 34 | 35 | private static void TryAddDialogButton(DialogAnswerView instance) 36 | { 37 | var transform = instance.AnswerText?.transform; 38 | 39 | #if DEBUG 40 | Debug.Log($"Adding/Removing dialog answer button on {instance.AnswerText?.name}..."); 41 | #endif 42 | 43 | var playButtonGameObject = transform?.Find(ButtonFactory.DIALOG_ANSWER_BUTTON_NAME)?.gameObject; 44 | var arrowTextureTransform = instance.transform.TryFind(DIALOG_ANSWER_ARROW_TEXTURE_PATH); 45 | 46 | Image arrowImage = null; 47 | if(arrowTextureTransform) 48 | arrowImage = arrowTextureTransform.GetComponent(); 49 | 50 | // 1. Check if the setting for showing the playback button is enabled. 51 | if (!Main.Settings.ShowPlaybackOfDialogAnswers) 52 | { 53 | // 1a. Destroy the button if it exists. 54 | if (playButtonGameObject != null) 55 | Object.Destroy(playButtonGameObject); 56 | // 1b. Re-enabled the arrow texture if it exists. 57 | if (arrowImage != null) 58 | arrowImage.enabled = true; 59 | 60 | return; 61 | } 62 | 63 | // 2. Check if the button already exists. 64 | if (playButtonGameObject != null) 65 | return; 66 | 67 | // 3. Create the button if it doesn't exist. 68 | playButtonGameObject = ButtonFactory.CreatePlayButton(transform, () => 69 | { 70 | if (instance.AnswerText == null) 71 | return; 72 | 73 | var text = instance.AnswerText.text; 74 | 75 | // Clean the text of tags 76 | text = Regex.Replace(text, "<(.*?)>", string.Empty); 77 | 78 | // Remove the number of the answer if the setting is disabled 79 | if (!Main.Settings.SayDialogAnswerNumber) 80 | text = new Regex(@"^(\d+\.)(.*)").Replace(text, "$2"); 81 | 82 | text = text.PrepareText(); 83 | 84 | var voiceType = VoiceType.Narrator; 85 | if (Main.Settings.UseGenderSpecificVoices && Game.Instance.DialogController.FirstSpeaker != null) // If we are speaking to a character 86 | voiceType = Game.Instance.Player.MainCharacter.Value.Gender == Gender.Female ? VoiceType.Female : VoiceType.Male; 87 | 88 | Main.Speech.SpeakAs(text, voiceType); 89 | }); 90 | 91 | if (playButtonGameObject == null || playButtonGameObject.transform == null) 92 | { 93 | Debug.LogWarning("Failed to create the dialog answer button!"); 94 | return; 95 | } 96 | 97 | if (Main.Settings?.DialogAnswerColorOnHover == true) 98 | { 99 | SetDialogAnswerColorHover(playButtonGameObject, instance); 100 | } 101 | 102 | playButtonGameObject.name = ButtonFactory.DIALOG_ANSWER_BUTTON_NAME; 103 | playButtonGameObject.transform.localScale = new Vector3(0.75f, 0.75f, 1f); 104 | playButtonGameObject.transform.localRotation = Quaternion.Euler(0, 0, 90); 105 | playButtonGameObject.RectAlignMiddleLeft(new Vector2(-18f, -12f)); 106 | playButtonGameObject.SetActive(true); 107 | 108 | //4. Disable the arrow image 109 | if (arrowImage != null) 110 | arrowImage.enabled = false; 111 | } 112 | 113 | private static void SetDialogAnswerColorHover(GameObject playButtonGameObject, DialogAnswerView instance) 114 | { 115 | var highlight = instance?.transform.TryFind("Highlight"); 116 | if (highlight == null) 117 | return; 118 | 119 | var highlightImage = highlight.GetComponent(); 120 | if (highlightImage == null) 121 | return; 122 | 123 | var colorOff = highlightImage.color; 124 | 125 | var button = playButtonGameObject.GetComponent(); 126 | if (button == null) 127 | return; 128 | 129 | button.OnHover.RemoveAllListeners(); 130 | button.OnHover.AddListener(hover => 131 | { 132 | var colorOn = new Color(Main.Settings!.DialogAnswerHoverColorR, Main.Settings!.DialogAnswerHoverColorG, Main.Settings!.DialogAnswerHoverColorB, Main.Settings!.DialogAnswerHoverColorA); 133 | if (hover) 134 | { 135 | highlightImage.color = colorOn; 136 | } 137 | else 138 | { 139 | button.StartCoroutine(ResetColor(highlightImage, colorOff)); 140 | } 141 | }); 142 | } 143 | 144 | private static IEnumerator ResetColor(Image image, Color color) 145 | { 146 | yield return new WaitForEndOfFrame(); 147 | image.color = color; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /SpeechMod/Patches/DialogController_Patch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Kingmaker; 3 | using Kingmaker.Controllers.Dialog; 4 | using Kingmaker.DialogSystem.Blueprints; 5 | #if DEBUG 6 | using UnityEngine; 7 | #endif 8 | 9 | namespace SpeechMod.Patches; 10 | 11 | [HarmonyPatch(typeof(DialogController), "SelectAnswer")] 12 | public class DialogController_Patch 13 | { 14 | public static void Prefix(BlueprintAnswer answer) 15 | { 16 | if (!Main.Enabled) 17 | return; 18 | 19 | #if DEBUG 20 | Debug.Log($"{nameof(DialogController)}_SelectAnswer_Prefix"); 21 | #endif 22 | 23 | if (!Main.Settings.AutoPlay) 24 | { 25 | #if DEBUG 26 | Debug.Log("Autoplay is disabled!"); 27 | #endif 28 | return; 29 | } 30 | 31 | // Don't stop voice if the dialog is closing or if it is not a normal dialog 32 | // However the playback will stop if a normal dialog is instigated while a non dialog playback is playing. 33 | // This is the same behaviour as in the old Infinity Engine games. 34 | if (Game.Instance.DialogController.Dialog.GetExitAnswer().Equals(answer) || 35 | Game.Instance.DialogController.Dialog.Type != DialogType.Common) 36 | return; 37 | 38 | Main.Speech.Stop(); 39 | } 40 | } -------------------------------------------------------------------------------- /SpeechMod/Patches/Dialog_Patch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Kingmaker; 3 | using Kingmaker.Localization; 4 | using Kingmaker.UI.MVVM._VM.Dialog.Dialog; 5 | #if DEBUG 6 | using UnityEngine; 7 | #endif 8 | 9 | namespace SpeechMod.Patches; 10 | 11 | [HarmonyPatch(typeof(DialogVM), "HandleOnCueShow")] 12 | public static class Dialog_Patch 13 | { 14 | public static void Postfix() 15 | { 16 | if (!Main.Enabled) 17 | return; 18 | 19 | #if DEBUG 20 | Debug.Log($"{nameof(DialogVM)}_HandleOnCueShow_Postfix"); 21 | #endif 22 | 23 | if (!Main.Settings.AutoPlay) 24 | { 25 | #if DEBUG 26 | Debug.Log($"{nameof(DialogVM)}: AutoPlay is disabled!"); 27 | #endif 28 | return; 29 | } 30 | 31 | var key = Game.Instance?.DialogController?.CurrentCue?.Text?.Key; 32 | if (string.IsNullOrWhiteSpace(key)) 33 | key = Game.Instance?.DialogController?.CurrentCue?.Text?.Shared?.String?.Key; 34 | 35 | if (string.IsNullOrWhiteSpace(key)) 36 | return; 37 | 38 | // Stop playing and don't play if the dialog is voice acted. 39 | if (!Main.Settings.AutoPlayIgnoreVoice && 40 | !string.IsNullOrWhiteSpace(LocalizationManager.SoundPack?.GetText(key, false))) 41 | { 42 | Main.Speech.Stop(); 43 | return; 44 | } 45 | 46 | Main.Speech.SpeakDialog(Game.Instance?.DialogController?.CurrentCue?.DisplayText, 0.5f); 47 | } 48 | } -------------------------------------------------------------------------------- /SpeechMod/Patches/Encyclopedia_Patch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Kingmaker.UI.MVVM._PCView.ServiceWindows.Encyclopedia; 3 | using SpeechMod.Unity; 4 | using System.Linq; 5 | using SpeechMod.Unity.Extensions; 6 | using TMPro; 7 | using UnityEngine; 8 | 9 | namespace SpeechMod.Patches; 10 | 11 | [HarmonyPatch] 12 | public static class Encyclopedia_Patch 13 | { 14 | private static readonly string m_ButtonName = "EncyclopediaSpeechButton"; 15 | 16 | private const string BODY_GROUP_PATH = "ServiceWindowsPCView/Background/Windows/EncyclopediaPCView/EncyclopediaPageView/BodyGroup"; 17 | 18 | [HarmonyPostfix] 19 | [HarmonyPatch(typeof(EncyclopediaPageBaseView), "Initialize")] 20 | public static void Initialize_Postfix(EncyclopediaPageBaseView __instance) 21 | { 22 | if (!Main.Enabled) 23 | return; 24 | 25 | __instance.m_Title.HookupTextToSpeech(); 26 | } 27 | 28 | [HarmonyPostfix] 29 | [HarmonyPatch(typeof(EncyclopediaPagePCView), "UpdateView")] 30 | public static void UpdateView_Postfix() 31 | { 32 | if (!Main.Enabled) 33 | return; 34 | 35 | #if DEBUG 36 | Debug.Log($"{nameof(EncyclopediaPagePCView)}_UpdateView_Postfix"); 37 | #endif 38 | 39 | var bodyGroup = UIHelper.TryFindInStaticCanvas(BODY_GROUP_PATH); 40 | if (bodyGroup == null) 41 | { 42 | #if DEBUG 43 | Debug.Log("Couldn't find BodyGroup..."); 44 | #endif 45 | return; 46 | } 47 | 48 | var content = bodyGroup.TryFind("ObjectivesGroup/StandardScrollView/Viewport/Content"); 49 | if (content == null) 50 | { 51 | #if DEBUG 52 | Debug.Log("Couldn't any TextMeshProUGUI..."); 53 | #endif 54 | return; 55 | } 56 | 57 | // Only get the texts that is not in the unit view. 58 | var allTexts = content.gameObject?.GetComponentsInChildren(true).Where(t => t.transform.name.Equals("Text")).ToArray(); 59 | if (allTexts == null || allTexts.Length == 0) 60 | { 61 | #if DEBUG 62 | Debug.Log("Couldn't find any TextMeshProUGUI..."); 63 | #endif 64 | return; 65 | } 66 | 67 | foreach (var textMeshPro in allTexts) 68 | { 69 | var parent = textMeshPro.transform; 70 | 71 | var button = parent.TryFind(m_ButtonName)?.gameObject; 72 | 73 | if (button != null) 74 | { 75 | #if DEBUG 76 | Debug.Log("Button already added, relocating and activating..."); 77 | #endif 78 | button.transform.localRotation = Quaternion.Euler(0, 0, 90); 79 | button.RectAlignTopLeft(); 80 | button.transform.localPosition = new Vector3(-36, -26, 0); 81 | continue; 82 | } 83 | 84 | #if DEBUG 85 | Debug.Log("Adding playbutton..."); 86 | #endif 87 | button = ButtonFactory.CreatePlayButton(parent, () => 88 | { 89 | Main.Speech.Speak(textMeshPro.text); 90 | }); 91 | button.name = m_ButtonName; 92 | button.transform.localRotation = Quaternion.Euler(0, 0, 90); 93 | button.RectAlignTopLeft(); 94 | button.transform.localPosition = new Vector3(-36, -26, 0); 95 | button.gameObject.SetActive(true); 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /SpeechMod/Patches/GlobalMapEnterMessage_Patch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Kingmaker.UI.MVVM._PCView.GlobalMap.Message; 3 | using SpeechMod.Unity.Extensions; 4 | using UnityEngine; 5 | 6 | namespace SpeechMod.Patches; 7 | 8 | [HarmonyPatch(typeof(GlobalMapEnterMessagePCView), "BindViewImplementation")] 9 | public class GlobalMapEnterMessage_Patch 10 | { 11 | public static void Postfix() 12 | { 13 | if (!Main.Enabled) 14 | return; 15 | 16 | #if DEBUG 17 | Debug.Log($"{nameof(GlobalMapEnterMessagePCView)}_BindViewImplementation_Postfix"); 18 | #endif 19 | 20 | var labelMessage = UIHelper.TryFindInStaticCanvas("GlobalMapMessageBoxView/Window/Layout/Label_Message"); 21 | if (labelMessage == null) 22 | { 23 | Debug.Log("Label_Message not found!"); 24 | return; 25 | } 26 | 27 | labelMessage.HookupTextToSpeechOnTransform(); 28 | } 29 | } -------------------------------------------------------------------------------- /SpeechMod/Patches/JournalQuestObjective_Patch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Kingmaker.UI.MVVM._PCView.ServiceWindows.Journal; 3 | using SpeechMod.Unity; 4 | using SpeechMod.Unity.Extensions; 5 | using TMPro; 6 | using UnityEngine; 7 | using UnityEngine.UI; 8 | 9 | namespace SpeechMod.Patches; 10 | 11 | [HarmonyPatch(typeof(JournalQuestObjectivePCView), "BindViewImplementation")] 12 | public static class JournalQuestObjective_Patch 13 | { 14 | private static readonly string m_ButtonName = "JQSpeechButton"; 15 | 16 | private const string BODY_GROUP_PATH = "ServiceWindowsPCView/Background/Windows/JournalPCView/JournalQuestView/BodyGroup"; 17 | 18 | public static void Postfix() 19 | { 20 | if (!Main.Enabled) 21 | return; 22 | 23 | #if DEBUG 24 | Debug.Log($"{nameof(JournalQuestObjectivePCView)}_BindViewImplementation_Postfix"); 25 | #endif 26 | 27 | var bodyGroup = UIHelper.TryFindInStaticCanvas(BODY_GROUP_PATH); 28 | if (bodyGroup == null) 29 | { 30 | Debug.Log("Couldn't find BodyGroup..."); 31 | return; 32 | } 33 | 34 | var allTexts = bodyGroup.gameObject.GetComponentsInChildren(true); 35 | if (allTexts == null || allTexts.Length == 0) 36 | { 37 | Debug.Log("Couldn't any TextMeshProUGUI..."); 38 | return; 39 | } 40 | 41 | var isFirst = true; 42 | foreach (var textMeshPro in allTexts) 43 | { 44 | var tmpTransform = textMeshPro.transform; 45 | if (!ShouldAddButton(tmpTransform)) 46 | continue; 47 | 48 | var button = tmpTransform?.TryFind(m_ButtonName)?.gameObject; 49 | 50 | if (button != null) 51 | { 52 | #if DEBUG 53 | Debug.Log("Button already added, relocating and activating..."); 54 | #endif 55 | button.transform.localRotation = Quaternion.Euler(0, 0, 90); 56 | tmpTransform.gameObject.RectAlignTopLeft(); 57 | button.RectAlignTopLeft(); 58 | SetNewPosition(tmpTransform, button.transform, ref isFirst); 59 | button.SetActive(true); 60 | continue; 61 | } 62 | 63 | #if DEBUG 64 | Debug.Log("Adding playbutton..."); 65 | #endif 66 | button = ButtonFactory.CreatePlayButton(tmpTransform.transform, () => 67 | { 68 | Main.Speech.Speak(textMeshPro.text); 69 | }); 70 | button.name = m_ButtonName; 71 | button.transform.localRotation = Quaternion.Euler(0, 0, 90); 72 | tmpTransform.gameObject.RectAlignTopLeft(); 73 | button.RectAlignTopLeft(); 74 | SetNewPosition(tmpTransform, button.transform, ref isFirst); 75 | button.SetActive(true); 76 | } 77 | 78 | // Move the line back behind our buttons. 79 | var allImages = bodyGroup.GetComponentsInChildren(); 80 | foreach (var image in allImages) 81 | { 82 | if (image.gameObject.name.Equals("LeftVerticalBorderImage")) 83 | image.transform.SetAsFirstSibling(); 84 | } 85 | } 86 | 87 | private static bool ShouldAddButton(Transform transform) 88 | { 89 | switch (transform.name) 90 | { 91 | case "LastChapterLabel": 92 | case "DescriptionLabel": 93 | case "Label": 94 | return true; 95 | default: 96 | return false; 97 | } 98 | } 99 | 100 | private static void SetNewPosition(Transform tmpTransform, Transform transform, ref bool isFirst) 101 | { 102 | switch (tmpTransform.name) 103 | { 104 | case "LastChapterLabel": 105 | transform.localPosition = new Vector3(-72, -35, 0); 106 | break; 107 | case "TitleLabel": 108 | transform.localPosition = new Vector3(0, -42, 0); 109 | break; 110 | case "DescriptionLabel": 111 | if (isFirst) 112 | { 113 | isFirst = false; 114 | transform.localPosition = new Vector3(-10, -24, 0); 115 | break; 116 | } 117 | transform.localPosition = new Vector3(-35, -24, 0); 118 | break; 119 | case "Label": 120 | var ipi = tmpTransform.parent.TryFind("InProgressImage").gameObject; 121 | transform.localPosition = new Vector3(-82, ipi.transform.InverseTransformPoint(ipi.transform.position).y - 26, 0); 122 | break; 123 | default: 124 | transform.localPosition = Vector3.zero; 125 | break; 126 | } 127 | } 128 | } -------------------------------------------------------------------------------- /SpeechMod/Patches/LoadingScreenPCView_Patch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Kingmaker.UI.MVVM._PCView.LoadingScreen; 3 | using SpeechMod.Unity.Extensions; 4 | #if DEBUG 5 | using UnityEngine; 6 | #endif 7 | 8 | namespace SpeechMod.Patches; 9 | 10 | [HarmonyPatch] 11 | public static class LoadingScreenPCView_Patch 12 | { 13 | [HarmonyPatch(typeof(LoadingScreenPCView), nameof(LoadingScreenPCView.BindViewImplementation))] 14 | [HarmonyPostfix] 15 | public static void BindViewImplementation_Postfix(LoadingScreenPCView __instance) 16 | { 17 | if (!Main.Enabled) 18 | return; 19 | 20 | #if DEBUG 21 | Debug.Log($"{nameof(LoadingScreenPCView)}_{nameof(BindViewImplementation_Postfix)}"); 22 | #endif 23 | 24 | __instance.m_CharacterDescriptionText.HookupTextToSpeech(); 25 | __instance.m_CharacterNameText.HookupTextToSpeech(); 26 | __instance.m_Hint.HookupTextToSpeech(); 27 | } 28 | } -------------------------------------------------------------------------------- /SpeechMod/Patches/LocalMapBaseView_Patch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Kingmaker.UI.MVVM._PCView.ServiceWindows.LocalMap; 3 | using SpeechMod.Unity.Extensions; 4 | 5 | namespace SpeechMod.Patches; 6 | 7 | [HarmonyPatch] 8 | public static class LocalMapBaseView_Patch 9 | { 10 | [HarmonyPostfix] 11 | [HarmonyPatch(typeof(LocalMapBaseView), "Initialize")] 12 | public static void Initialize_Postfix(LocalMapBaseView __instance) 13 | { 14 | if (!Main.Enabled) 15 | return; 16 | 17 | __instance.m_Title.HookupTextToSpeech(); 18 | __instance.m_LocationTypeText.HookupTextToSpeech(); 19 | } 20 | } -------------------------------------------------------------------------------- /SpeechMod/Patches/MessageModal_Patch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Kingmaker.UI.MVVM._PCView.Common.MessageModal; 3 | using SpeechMod.Unity.Extensions; 4 | using UnityEngine; 5 | 6 | namespace SpeechMod.Patches; 7 | 8 | [HarmonyPatch(typeof(MessageModalPCView), "BindViewImplementation")] 9 | public class MessageModal_Patch 10 | { 11 | public static void Postfix() 12 | { 13 | if (!Main.Enabled) 14 | return; 15 | 16 | #if DEBUG 17 | Debug.Log($"{nameof(MessageModalPCView)}_BindViewImplementation_Postfix"); 18 | #endif 19 | 20 | var labelMessage = UIHelper.TryFindInFadeCanvas("MessageModalPCView/WindowContainer/Layout/Label_Message"); 21 | if (labelMessage == null) 22 | { 23 | Debug.Log("Label_Message not found!"); 24 | return; 25 | } 26 | 27 | labelMessage.HookupTextToSpeechOnTransform(); 28 | } 29 | } -------------------------------------------------------------------------------- /SpeechMod/Patches/NewGamePhaseStoryPCView_Patch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Kingmaker.UI.MVVM._PCView.NewGame.Story; 3 | using SpeechMod.Unity.Extensions; 4 | #if DEBUG 5 | using UnityEngine; 6 | #endif 7 | 8 | namespace SpeechMod.Patches; 9 | 10 | [HarmonyPatch] 11 | public static class NewGamePhaseStoryPCView_Patch 12 | { 13 | [HarmonyPatch(typeof(NewGamePhaseStoryPCView), nameof(NewGamePhaseStoryPCView.BindViewImplementation))] 14 | [HarmonyPostfix] 15 | public static void BindViewImplementation_Postfix(NewGamePhaseStoryPCView __instance) 16 | { 17 | if (!Main.Enabled) 18 | return; 19 | #if DEBUG 20 | Debug.Log($"{nameof(NewGamePhaseStoryPCView)}_{nameof(BindViewImplementation_Postfix)}"); 21 | #endif 22 | 23 | __instance.m_Description.HookupTextToSpeech(); 24 | __instance.m_DlcRequiredText.HookupTextToSpeech(); 25 | __instance.m_LastAzlantiText.HookupTextToSpeech(); 26 | //__instance.m_SelectorStoryText.HookupTextToSpeech(); 27 | } 28 | } -------------------------------------------------------------------------------- /SpeechMod/Patches/SettingsEntityDropdownGameDifficultyItemPCView_Patch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Kingmaker.UI.MVVM._PCView.Settings.Entities.Difficulty; 3 | using SpeechMod.Unity.Extensions; 4 | #if DEBUG 5 | using UnityEngine; 6 | #endif 7 | 8 | namespace SpeechMod.Patches; 9 | 10 | [HarmonyPatch] 11 | public static class SettingsEntityDropdownGameDifficultyItemPCView_Patch 12 | { 13 | [HarmonyPatch(typeof(SettingsEntityDropdownGameDifficultyItemPCView), nameof(SettingsEntityDropdownGameDifficultyItemPCView.BindViewImplementation))] 14 | [HarmonyPostfix] 15 | public static void BindViewImplementation_Postfix(SettingsEntityDropdownGameDifficultyItemPCView __instance) 16 | { 17 | if (!Main.Enabled) 18 | return; 19 | #if DEBUG 20 | Debug.Log($"{nameof(SettingsEntityDropdownGameDifficultyItemPCView)}_{nameof(BindViewImplementation_Postfix)}"); 21 | #endif 22 | var textToRead = $"{__instance.ViewModel.Title}. {__instance.ViewModel.Description}"; 23 | 24 | __instance.m_Title.HookupTextToSpeech(textToRead); 25 | } 26 | } -------------------------------------------------------------------------------- /SpeechMod/Patches/SettingsEntityView_Patch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Kingmaker.UI.MVVM._PCView.Settings.Entities; 3 | using Kingmaker.UI.MVVM._VM.Settings.Entities; 4 | using SpeechMod.Unity.Extensions; 5 | #if DEBUG 6 | using UnityEngine; 7 | #endif 8 | 9 | namespace SpeechMod.Patches; 10 | 11 | [HarmonyPatch] 12 | public static class SettingsEntityView_Patch 13 | { 14 | [HarmonyPatch(typeof(SettingsEntityView), nameof(SettingsEntityView.BindViewImplementation))] 15 | [HarmonyPostfix] 16 | public static void BindViewImplementation_Postfix(SettingsEntityView __instance) 17 | { 18 | if (!Main.Enabled) 19 | return; 20 | #if DEBUG 21 | Debug.Log($"{nameof(SettingsEntityView)}_{nameof(BindViewImplementation_Postfix)}"); 22 | #endif 23 | var textToRead = $"{__instance.ViewModel.Title}. {__instance.ViewModel.Description}"; 24 | 25 | __instance.m_Title.HookupTextToSpeech(textToRead); 26 | } 27 | } -------------------------------------------------------------------------------- /SpeechMod/Patches/StaticCanvas_Patch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Kingmaker; 3 | using Kingmaker.UI; 4 | using SpeechMod.Unity; 5 | using SpeechMod.Unity.Extensions; 6 | using UnityEngine; 7 | 8 | namespace SpeechMod.Patches; 9 | 10 | [HarmonyPatch(typeof(StaticCanvas), "Initialize")] 11 | public static class StaticCanvas_Patch 12 | { 13 | private const string SCROLL_VIEW_PATH = "NestedCanvas1/DialogPCView/Body/View/Scroll View"; 14 | 15 | public static void Postfix() 16 | { 17 | if (!Main.Enabled) 18 | return; 19 | 20 | #if DEBUG 21 | var sceneName = Game.Instance.CurrentlyLoadedArea.ActiveUIScene.SceneName; 22 | Debug.Log($"{nameof(StaticCanvas)}_Initialize_Postfix @ {sceneName}"); 23 | #endif 24 | 25 | AddDialogSpeechButton(); 26 | } 27 | 28 | private static void AddDialogSpeechButton() 29 | { 30 | 31 | #if DEBUG 32 | Debug.Log("Adding speech button to dialog ui."); 33 | #endif 34 | 35 | var parent = UIHelper.TryFindInStaticCanvas(SCROLL_VIEW_PATH); 36 | 37 | if (parent == null) 38 | { 39 | Debug.LogWarning("Parent not found!"); 40 | return; 41 | } 42 | 43 | var buttonGameObject = ButtonFactory.CreatePlayButton(parent, () => 44 | { 45 | Main.Speech.SpeakDialog(Game.Instance?.DialogController?.CurrentCue?.DisplayText); 46 | }); 47 | 48 | buttonGameObject.name = "SpeechButton"; 49 | buttonGameObject.transform.localPosition = new Vector3(-493, 164, 0); 50 | buttonGameObject.transform.localRotation = Quaternion.Euler(0, 0, 90); 51 | 52 | buttonGameObject.SetActive(true); 53 | } 54 | } -------------------------------------------------------------------------------- /SpeechMod/Patches/TooltipEngine_Patch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Kingmaker.UI.MVVM._PCView.Tooltip.Bricks; 3 | using Kingmaker.UI.MVVM._VM.Tooltip.Utils; 4 | using SpeechMod.Unity.Extensions; 5 | using UnityEngine; 6 | 7 | namespace SpeechMod.Patches; 8 | 9 | [HarmonyPatch] 10 | static class TooltipEngine_Patch 11 | { 12 | [HarmonyPostfix] 13 | [HarmonyPatch(typeof(TooltipEngine), nameof(TooltipEngine.GetBrickView))] 14 | public static void GetBrickView_Postfix(ref MonoBehaviour __result) 15 | { 16 | if (!Main.Enabled) 17 | return; 18 | 19 | if (__result == null) 20 | return; 21 | 22 | #if DEBUG 23 | Debug.Log($"{nameof(TooltipEngine)}{nameof(TooltipEngine.GetBrickView)}:{__result.GetType().Name} @ {__result.transform.GetGameObjectPath()}"); 24 | #endif 25 | 26 | // TODO: Possibly add more types, however it seems the text in those are split 27 | if (__result is not ( 28 | TooltipBrickTextView or 29 | TooltipBrickEntityHeaderView or 30 | TooltipBrickIconAndNameView or 31 | TooltipBrickTitleView or 32 | TooltipBrickItemFooterView or 33 | TooltipBrickIconValueStatView or 34 | TooltipBrickValueStatFormulaView or 35 | TooltipBrickTimerView or 36 | TooltipBrickPortraitAndNameView or 37 | TooltipBrickShortClassDescriptionView or 38 | TooltipBrickFeatureShortDescriptionView 39 | )) 40 | return; 41 | 42 | #if DEBUG 43 | Debug.Log($"{nameof(TooltipEngine)}{nameof(TooltipEngine.GetBrickView)}:{__result.transform.GetGameObjectPath()}"); 44 | #endif 45 | 46 | __result.gameObject.transform.HookupTextToSpeechOnTransform(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /SpeechMod/Patches/TutorialWindowView_Patch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Kingmaker.UI; 3 | using Kingmaker.UI.MVVM._PCView.Tutorial; 4 | using Kingmaker.UI.MVVM._VM.Tutorial; 5 | using SpeechMod.Unity.Extensions; 6 | using UnityEngine; 7 | 8 | namespace SpeechMod.Patches; 9 | 10 | [HarmonyPatch(typeof(TutorialWindowView), nameof(TutorialWindowView.SetPage))] 11 | public class TutorialWindowView_Patch 12 | { 13 | public static void Postfix(TutorialWindowView __instance) 14 | { 15 | if (!Main.Enabled) 16 | return; 17 | 18 | #if DEBUG 19 | Debug.Log($"{nameof(TutorialWindowView)}_{nameof(TutorialWindowView.SetPage)}_Postfix"); 20 | #endif 21 | 22 | var smallWindow = UIHelper.TryFindInFadeCanvas("TutorialView/SmallWindow"); 23 | var bigWindow = UIHelper.TryFindInFadeCanvas("TutorialView/BigWindow"); 24 | 25 | if (smallWindow == null && bigWindow == null) 26 | Debug.LogWarning("Postfix on SetPage but both small and big tutorial window is null!"); 27 | 28 | if (smallWindow != null && smallWindow.gameObject.activeInHierarchy) 29 | HookSmallWindow(smallWindow); 30 | 31 | if (bigWindow != null && bigWindow.gameObject.activeInHierarchy) 32 | HookBigWindow(bigWindow); 33 | } 34 | 35 | private static void HookSmallWindow(Transform smallWindow) 36 | { 37 | #if DEBUG 38 | Debug.Log("Hooking on SMALL tutorial window!"); 39 | #endif 40 | 41 | var content = smallWindow.TryFind("Window/Content/Body/ScrollView/ViewPort/Content"); 42 | if (content == null) 43 | { 44 | #if DEBUG 45 | Debug.LogWarning("Content of SMALL tutorial window was not found!"); 46 | #endif 47 | return; 48 | } 49 | 50 | content.HookupTextToSpeechOnTransform(); 51 | 52 | var viewPort = smallWindow.TryFind("Window/Content/Body/ScrollView/ViewPort"); 53 | if (viewPort == null) 54 | { 55 | #if DEBUG 56 | Debug.LogWarning("ViewPort of SMALL tutorial window was not found!"); 57 | #endif 58 | return; 59 | } 60 | 61 | // Disable after first arrangement so when hovering buttons or links the view doesn't jump to top. 62 | var vlgw = viewPort.GetComponent(); 63 | if (vlgw == null) 64 | return; 65 | 66 | vlgw.CalculateLayoutInputHorizontal(); 67 | 68 | // Delay the disabling of the script until it has had a chance to run. 69 | var monoBehaviour = viewPort.GetComponent(); 70 | monoBehaviour.ExecuteLater(0.5f, () => { vlgw.enabled = false; }); 71 | } 72 | 73 | private static void HookBigWindow(Transform bigWindow) 74 | { 75 | #if DEBUG 76 | Debug.Log("Hooking on BIG tutorial window!"); 77 | #endif 78 | var rightSideTutorialDescription = bigWindow.TryFind("Window/Content/Body/RightSide/Description"); 79 | if (rightSideTutorialDescription == null) 80 | { 81 | #if DEBUG 82 | Debug.LogWarning("Right side description of BIG tutorial window was not found!"); 83 | #endif 84 | return; 85 | } 86 | 87 | rightSideTutorialDescription.HookupTextToSpeechOnTransform(); 88 | } 89 | } -------------------------------------------------------------------------------- /SpeechMod/PhoneticDictionary.json: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /SpeechMod/Settings.cs: -------------------------------------------------------------------------------- 1 | using UnityModManagerNet; 2 | 3 | namespace SpeechMod; 4 | 5 | public class Settings : UnityModManager.ModSettings 6 | { 7 | public int NarratorRate = 0; 8 | public int NarratorVolume = 100; 9 | public int NarratorPitch = 0; 10 | 11 | public int FemaleRate = 0; 12 | public int FemaleVolume = 100; 13 | public int FemalePitch = 0; 14 | 15 | public int MaleRate = 0; 16 | public int MaleVolume = 100; 17 | public int MalePitch = 0; 18 | 19 | public bool AutoPlay = false; 20 | public bool AutoPlayIgnoreVoice = false; 21 | 22 | public string[] AvailableVoices; 23 | public int NarratorVoice = 0; 24 | 25 | public bool UseGenderSpecificVoices = false; 26 | public int FemaleVoice = 0; 27 | public int MaleVoice = 0; 28 | 29 | public bool ColorOnHover = false; 30 | public float HoverColorR = 0f; 31 | public float HoverColorG = 0f; 32 | public float HoverColorB = 0f; 33 | public float HoverColorA = 1; 34 | 35 | public bool FontStyleOnHover = true; 36 | public bool[] FontStyles = [false, false, false, true, false, false, false, false, false, false, false]; 37 | 38 | public bool InterruptPlaybackOnPlay = true; 39 | 40 | public bool ShowNotificationOnPlaybackStop = true; 41 | 42 | public bool ShowPlaybackOfDialogAnswers = true; 43 | public bool SayDialogAnswerNumber = false; 44 | public bool DialogAnswerColorOnHover = true; 45 | public float DialogAnswerHoverColorR = 0.8f; 46 | public float DialogAnswerHoverColorG = 0.78f; 47 | public float DialogAnswerHoverColorB = 0.5f; 48 | public float DialogAnswerHoverColorA = 0.75f; 49 | 50 | public override void Save(UnityModManager.ModEntry modEntry) 51 | { 52 | Save(this, modEntry); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /SpeechMod/SpeechMod.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | net481 6 | PathfinderWOTRSpeechMod 7 | Pathfinder : Wrath of the Righteous - SpeechMod 8 | 1.1.1 9 | true 10 | latest 11 | SpeechMod 12 | false 13 | $(LocalAppData)Low\Owlcat Games\Pathfinder Wrath Of The Righteous 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | PreserveNewest 44 | 45 | 46 | 47 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | ^Mono path\[0\] = '(.*?)/Wrath_Data/Managed'$ 59 | 60 | 61 | $([System.Text.RegularExpressions.Regex]::Match($(MonoPathLine), $(MonoPathRegex)).Groups[1].Value) 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /SpeechMod/Unity/AppleVoiceUnity.cs: -------------------------------------------------------------------------------- 1 | using Kingmaker; 2 | using Kingmaker.Blueprints; 3 | using System; 4 | using System.Diagnostics; 5 | using System.Text.RegularExpressions; 6 | using SpeechMod.Unity.Extensions; 7 | using UnityEngine; 8 | 9 | namespace SpeechMod.Unity; 10 | 11 | public class AppleVoiceUnity : MonoBehaviour 12 | { 13 | private static AppleVoiceUnity _theVoice; 14 | 15 | private static string GenderVoice => Game.Instance?.DialogController?.CurrentSpeaker?.Gender == Gender.Female ? Main.FemaleVoice : Main.MaleVoice; 16 | private static int GenderRate => Game.Instance?.DialogController?.CurrentSpeaker?.Gender == Gender.Female ? Main.Settings.FemaleRate : Main.Settings.MaleRate; 17 | 18 | private static Process _speakingProcess; 19 | 20 | private static bool IsVoiceInitialized() 21 | { 22 | if (_theVoice != null) 23 | return true; 24 | 25 | Main.Logger.Critical("No voice initialized!"); 26 | return false; 27 | } 28 | 29 | void Start() 30 | { 31 | if (_theVoice != null) 32 | Destroy(gameObject); 33 | else 34 | _theVoice = this; 35 | } 36 | 37 | public static void Speak(string text, float delay = 0f) 38 | { 39 | if (!IsVoiceInitialized()) 40 | return; 41 | 42 | if (delay > 0f) 43 | { 44 | _theVoice.ExecuteLater(delay, () => Speak(text)); 45 | return; 46 | } 47 | 48 | Stop(); 49 | 50 | _speakingProcess = Process.Start("/usr/bin/say", text); 51 | } 52 | 53 | public static void SpeakDialog(string text, float delay = 0f) 54 | { 55 | if (!IsVoiceInitialized()) 56 | return; 57 | 58 | if (delay > 0f) 59 | { 60 | _theVoice.ExecuteLater(delay, () => SpeakDialog(text)); 61 | return; 62 | } 63 | 64 | var arguments = ""; 65 | text = new Regex("]+>]+)?>([^<>]*)").Replace(text, "$2"); 66 | text = text.Replace("\\n", " "); 67 | text = text.Replace("\n", " "); 68 | text = text.Replace(";", ""); 69 | while (text.IndexOf("", StringComparison.InvariantCultureIgnoreCase) != -1) 70 | { 71 | var position = text.IndexOf("", StringComparison.InvariantCultureIgnoreCase); 72 | if (position != 0) 73 | { 74 | var argumentsPart = text.Substring(0, position); 75 | text = text.Substring(position); 76 | arguments = $"{arguments}say -v {GenderVoice} -r {GenderRate} {argumentsPart.Replace("\"", "")};"; 77 | } 78 | else 79 | { 80 | position = text.IndexOf("", StringComparison.InvariantCultureIgnoreCase); 81 | var argumentsPart2 = text.Substring(0, position); 82 | text = text.Substring(position); 83 | arguments = $"{arguments}say -v {Main.NarratorVoice} -r {Main.Settings.NarratorRate} {argumentsPart2.Replace("\"", "")};"; 84 | } 85 | } 86 | 87 | text = text.Replace("\"", ""); 88 | if (!string.IsNullOrWhiteSpace(text) && text != "") 89 | arguments = $"{arguments}say -v {GenderVoice} -r {GenderRate} {text};"; 90 | 91 | arguments = new Regex("<[^>]+>").Replace(arguments, ""); 92 | 93 | KillAll(); 94 | 95 | arguments = "-c \"" + arguments + "\""; 96 | _speakingProcess = Process.Start("/bin/bash", arguments); 97 | } 98 | 99 | public static void Stop() 100 | { 101 | if (!IsVoiceInitialized()) 102 | return; 103 | 104 | _speakingProcess.Kill(); 105 | KillAll(); 106 | } 107 | 108 | public static bool IsSpeaking() 109 | { 110 | return !_speakingProcess.HasExited; 111 | } 112 | 113 | private static void KillAll() 114 | { 115 | Process.Start("/usr/bin/killall", "bash -kill"); 116 | Process.Start("/usr/bin/killall", "say -kill"); 117 | } 118 | } -------------------------------------------------------------------------------- /SpeechMod/Unity/ButtonFactory.cs: -------------------------------------------------------------------------------- 1 | using Kingmaker.UI.Common; 2 | using Kingmaker.UI.MVVM._VM.Tooltip.Templates; 3 | using Kingmaker.UI.MVVM._VM.Tooltip.Utils; 4 | using Owlcat.Runtime.UI.Controls.Button; 5 | using SpeechMod.Unity.Extensions; 6 | using UnityEngine; 7 | using UnityEngine.Events; 8 | using UnityEngine.UI; 9 | using Object = UnityEngine.Object; 10 | 11 | namespace SpeechMod.Unity; 12 | 13 | public static class ButtonFactory 14 | { 15 | public const string DIALOG_ANSWER_BUTTON_NAME = "SpeechMod_DialogAnswerButton"; 16 | 17 | private const string ARROW_BUTTON_PATH = "NestedCanvas1/DialogPCView/Body/View/Scroll View/ButtonEdge"; 18 | private const string ARROW_BUTTON_MAP_PATH = "CombatLog_New/Panel/ButtonEdge"; 19 | private const string MIRROR_MAP_PATH = "BookEventView/ContentWrapper/Window/Mirror/Mirror"; 20 | private const string MIRROR_STATIC_CANVAS_PATH = "NestedCanvas1/BookEventPCView/ContentWrapper/Window/Mirror/Mirror"; 21 | 22 | public static GameObject CreatePlayButton(Transform parent, UnityAction call) 23 | { 24 | return CreatePlayButton(parent, call, null, null); 25 | } 26 | 27 | private static GameObject CreatePlayButton(Transform parent, UnityAction call, string text, string toolTip) 28 | { 29 | GameObject arrowButton; 30 | 31 | if (UIUtility.IsGlobalMap()) 32 | { 33 | arrowButton = UIHelper.TryFindInStaticCanvas(ARROW_BUTTON_MAP_PATH)?.gameObject; 34 | var mirror = UIHelper.TryFindInStaticCanvas(MIRROR_MAP_PATH); 35 | FixMirrorRaycastTarget(mirror); 36 | } 37 | else 38 | { 39 | arrowButton = UIHelper.TryFindInStaticCanvas(ARROW_BUTTON_PATH)?.gameObject; 40 | var mirror = UIHelper.TryFindInStaticCanvas(MIRROR_STATIC_CANVAS_PATH); 41 | FixMirrorRaycastTarget(mirror); 42 | } 43 | 44 | if (arrowButton == null) 45 | return null; 46 | 47 | var buttonGameObject = Object.Instantiate(arrowButton, parent); 48 | SetupOwlcatButton(buttonGameObject, call, text, toolTip); 49 | 50 | return buttonGameObject; 51 | } 52 | 53 | private static void FixMirrorRaycastTarget(Transform mirror) 54 | { 55 | if (mirror != null) 56 | { 57 | var image = mirror.GetComponent(); 58 | if (image != null) 59 | { 60 | image.raycastTarget = false; 61 | } 62 | else 63 | { 64 | Debug.LogWarning("Image component not found in Mirror!"); 65 | } 66 | } 67 | else 68 | { 69 | Debug.LogWarning("Mirror not found in GlobalMap Static Canvas!"); 70 | } 71 | } 72 | 73 | private static void SetupOwlcatButton(GameObject buttonGameObject, UnityAction call, string text, string toolTip) 74 | { 75 | var button = buttonGameObject.GetComponent(); 76 | button.OnLeftClick.RemoveAllListeners(); 77 | 78 | if (button.OnLeftClick.GetPersistentEventCount() > 0) 79 | button.OnLeftClick.SetPersistentListenerState(0, UnityEventCallState.Off); 80 | 81 | button.OnLeftClick.AddListener(call); 82 | 83 | if (!string.IsNullOrWhiteSpace(text)) 84 | button.SetTooltip(new TooltipTemplateSimple(text, toolTip)); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /SpeechMod/Unity/Extensions/Hooks.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using TMPro; 3 | using UniRx; 4 | using UniRx.Triggers; 5 | using UnityEngine; 6 | 7 | namespace SpeechMod.Unity.Extensions; 8 | 9 | public static class Hooks 10 | { 11 | public static void HookUpTextToSpeechOnTransformWithPath(string path, bool force = false) 12 | { 13 | var transform = UIHelper.TryFind(path); 14 | if (transform == null) 15 | { 16 | Debug.LogWarning($"GameObject on '{path}' was not found!"); 17 | return; 18 | } 19 | 20 | HookupTextToSpeechOnTransform(transform, force); 21 | } 22 | 23 | public static void HookupTextToSpeechOnTransform(this Transform transform, bool force = false) 24 | { 25 | if (transform == null) 26 | { 27 | Debug.LogWarning("Can't hook up text to speech on null transform!"); 28 | return; 29 | } 30 | 31 | var path = transform.GetGameObjectPath(); 32 | 33 | var allTexts = transform.GetComponentsInChildren(true); 34 | if (allTexts?.Length == 0) 35 | { 36 | Debug.LogWarning($"No TextMeshProUGUI found in children on '{path}'!"); 37 | return; 38 | } 39 | 40 | allTexts.HookupTextToSpeech(force); 41 | } 42 | 43 | public static void HookupTextToSpeech(this TextMeshProUGUI[] textMeshPros, bool force = false) 44 | { 45 | if (textMeshPros == null) 46 | { 47 | Debug.LogWarning("No TextMeshProUGUIs to hook up!"); 48 | return; 49 | } 50 | 51 | foreach (var textMeshPro in textMeshPros) 52 | { 53 | textMeshPro.HookupTextToSpeech(null, force); 54 | } 55 | } 56 | 57 | public static void HookupTextToSpeech(this TextMeshProUGUI textMeshPro, string textOverride = null, bool force = false) 58 | { 59 | if (textMeshPro == null) 60 | { 61 | Debug.LogWarning("No TextMeshProUGUI!"); 62 | return; 63 | } 64 | 65 | var textMeshProTransform = textMeshPro.transform; 66 | if (textMeshProTransform == null) 67 | { 68 | Debug.LogWarning("Transform on TextMeshProUGUI is null!"); 69 | return; 70 | } 71 | 72 | // If the parent is clickable, don't hook up the text to speech, unless forced. 73 | if (!force && textMeshProTransform.IsParentClickable()) 74 | { 75 | return; 76 | } 77 | 78 | // Add the hook data to the text mesh pro. If it already exists, return. 79 | if (!textMeshProTransform.gameObject.TryAddComponent(out var hookData)) 80 | return; 81 | 82 | // Save the text style, color and extra padding 83 | hookData.FontStyles = textMeshPro.fontStyle; 84 | hookData.Color = textMeshPro.color; 85 | hookData.ExtraPadding = textMeshPro.extraPadding; 86 | 87 | // Make the text mesh pro clickable 88 | textMeshPro.raycastTarget = true; 89 | 90 | // Subscribe to the pointer enter, and add it to the disposables for ensuring it gets cleaned up. 91 | textMeshPro.OnPointerEnterAsObservable() 92 | .Subscribe( 93 | _ => 94 | { 95 | // Update the text style, color and extra padding 96 | hookData.FontStyles = textMeshPro.fontStyle; 97 | hookData.Color = textMeshPro.color; 98 | hookData.ExtraPadding = textMeshPro.extraPadding; 99 | 100 | if (Main.Settings!.FontStyleOnHover) 101 | { 102 | for (var i = 0; i < Main.Settings.FontStyles!.Length; i++) 103 | { 104 | if (Main.Settings.FontStyles[i]) 105 | { 106 | textMeshPro.fontStyle ^= (FontStyles)Enum.Parse(typeof(FontStyles), Main.FontStyleNames![i]!, true); 107 | } 108 | } 109 | 110 | textMeshPro.extraPadding = false; 111 | } 112 | 113 | if (Main.Settings.ColorOnHover) 114 | { 115 | textMeshPro.color = UIHelper.HoverColor; 116 | } 117 | } 118 | ).AddTo(hookData.Disposables); 119 | 120 | // Subscribe to the pointer exit, and add it to the disposables for ensuring it gets cleaned up. 121 | textMeshPro.OnPointerExitAsObservable().Subscribe( 122 | _ => 123 | { 124 | textMeshPro.fontStyle = hookData.FontStyles; 125 | textMeshPro.color = hookData.Color; 126 | textMeshPro.extraPadding = hookData.ExtraPadding; 127 | } 128 | ).AddTo(hookData.Disposables); 129 | 130 | // Subscribe to the pointer click, and add it to the disposables for ensuring it gets cleaned up. 131 | textMeshPro.OnPointerClickAsObservable().Subscribe( 132 | clickEvent => 133 | { 134 | if (clickEvent?.button == UnityEngine.EventSystems.PointerEventData.InputButton.Left) 135 | { 136 | Main.Speech?.Speak(string.IsNullOrWhiteSpace(textOverride) ? textMeshPro.text : textOverride); 137 | } 138 | } 139 | ).AddTo(hookData.Disposables); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /SpeechMod/Unity/Extensions/Transforms.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace SpeechMod.Unity.Extensions; 4 | 5 | public static class Transforms 6 | { 7 | //------------Top------------------- 8 | public static void RectAlignTopLeft(this GameObject uiObject, Vector2? anchoredPosition = null) 9 | { 10 | var anchorMin = new Vector2(0, 1); 11 | var anchorMax = new Vector2(0, 1); 12 | var pivot = new Vector2(0, 1); 13 | 14 | SetRectAlign(uiObject, anchorMin, anchorMax, pivot, anchoredPosition); 15 | } 16 | 17 | public static void RectAlignTopMiddle(this GameObject uiObject, Vector2? anchoredPosition = null) 18 | { 19 | var anchorMin = new Vector2(0.5f, 1); 20 | var anchorMax = new Vector2(0.5f, 1); 21 | var pivot = new Vector2(0.5f, 1); 22 | 23 | SetRectAlign(uiObject, anchorMin, anchorMax, pivot, anchoredPosition); 24 | } 25 | 26 | public static void RectAlignTopRight(this GameObject uiObject, Vector2? anchoredPosition = null) 27 | { 28 | var anchorMin = new Vector2(1, 1); 29 | var anchorMax = new Vector2(1, 1); 30 | var pivot = new Vector2(1, 1); 31 | 32 | SetRectAlign(uiObject, anchorMin, anchorMax, pivot, anchoredPosition); 33 | } 34 | 35 | //------------Middle------------------- 36 | public static void RectAlignMiddleLeft(this GameObject uiObject, Vector2? anchoredPosition = null) 37 | { 38 | var anchorMin = new Vector2(0, 0.5f); 39 | var anchorMax = new Vector2(0, 0.5f); 40 | var pivot = new Vector2(0, 0.5f); 41 | 42 | SetRectAlign(uiObject, anchorMin, anchorMax, pivot, anchoredPosition); 43 | } 44 | 45 | public static void RectAlignMiddle(this GameObject uiObject, Vector2? anchoredPosition = null) 46 | { 47 | var anchorMin = new Vector2(0.5f, 0.5f); 48 | var anchorMax = new Vector2(0.5f, 0.5f); 49 | var pivot = new Vector2(0.5f, 0.5f); 50 | 51 | SetRectAlign(uiObject, anchorMin, anchorMax, pivot, anchoredPosition); 52 | } 53 | 54 | public static void RectAlignMiddleRight(this GameObject uiObject, Vector2? anchoredPosition = null) 55 | { 56 | var anchorMin = new Vector2(1, 0.5f); 57 | var anchorMax = new Vector2(1, 0.5f); 58 | var pivot = new Vector2(1, 0.5f); 59 | 60 | SetRectAlign(uiObject, anchorMin, anchorMax, pivot, anchoredPosition); 61 | } 62 | 63 | //------------Bottom------------------- 64 | public static void RectAlignBottomLeft(this GameObject uiObject, Vector2? anchoredPosition = null) 65 | { 66 | var anchorMin = new Vector2(0, 0); 67 | var anchorMax = new Vector2(0, 0); 68 | var pivot = new Vector2(0, 0); 69 | 70 | SetRectAlign(uiObject, anchorMin, anchorMax, pivot, anchoredPosition); 71 | } 72 | 73 | public static void RectAlignBottomMiddle(this GameObject uiObject, Vector2? anchoredPosition = null) 74 | { 75 | var anchorMin = new Vector2(0.5f, 0); 76 | var anchorMax = new Vector2(0.5f, 0); 77 | var pivot = new Vector2(0.5f, 0); 78 | 79 | SetRectAlign(uiObject, anchorMin, anchorMax, pivot, anchoredPosition); 80 | } 81 | 82 | public static void RectAlignBottomRight(this GameObject uiObject, Vector2? anchoredPosition = null) 83 | { 84 | var anchorMin = new Vector2(1, 0); 85 | var anchorMax = new Vector2(1, 0); 86 | var pivot = new Vector2(1, 0); 87 | 88 | SetRectAlign(uiObject, anchorMin, anchorMax, pivot, anchoredPosition); 89 | } 90 | 91 | private static void SetRectAlign(GameObject uiObject, Vector2 anchorMin, Vector2 anchorMax, Vector2 pivot, Vector2? anchoredPosition = null) 92 | { 93 | var uitransform = uiObject.GetComponent(); 94 | 95 | if (uitransform == null) 96 | return; 97 | 98 | uitransform.anchorMin = anchorMin; 99 | uitransform.anchorMax = anchorMax; 100 | uitransform.pivot = pivot; 101 | 102 | if (anchoredPosition.HasValue) 103 | uitransform.anchoredPosition = anchoredPosition.Value; 104 | } 105 | 106 | public static void SetDefaultScale(this RectTransform trans) 107 | { 108 | trans.localScale = new Vector3(1, 1, 1); 109 | } 110 | public static void SetPivotAndAnchors(this RectTransform trans, Vector2 aVec) 111 | { 112 | trans.pivot = aVec; 113 | trans.anchorMin = aVec; 114 | trans.anchorMax = aVec; 115 | } 116 | 117 | public static Vector2 GetSize(this RectTransform trans) 118 | { 119 | return trans.rect.size; 120 | } 121 | public static float GetWidth(this RectTransform trans) 122 | { 123 | return trans.rect.width; 124 | } 125 | public static float GetHeight(this RectTransform trans) 126 | { 127 | return trans.rect.height; 128 | } 129 | 130 | public static void SetPositionOfPivot(this RectTransform trans, Vector2 newPos) 131 | { 132 | trans.localPosition = new Vector3(newPos.x, newPos.y, trans.localPosition.z); 133 | } 134 | 135 | public static void SetLeftBottomPosition(this RectTransform trans, Vector2 newPos) 136 | { 137 | trans.localPosition = new Vector3(newPos.x + (trans.pivot.x * trans.rect.width), newPos.y + (trans.pivot.y * trans.rect.height), trans.localPosition.z); 138 | } 139 | 140 | public static void SetLeftTopPosition(this RectTransform trans, Vector2 newPos) 141 | { 142 | trans.localPosition = new Vector3(newPos.x + (trans.pivot.x * trans.rect.width), newPos.y - ((1f - trans.pivot.y) * trans.rect.height), trans.localPosition.z); 143 | } 144 | 145 | public static void SetRightBottomPosition(this RectTransform trans, Vector2 newPos) 146 | { 147 | trans.localPosition = new Vector3(newPos.x - ((1f - trans.pivot.x) * trans.rect.width), newPos.y + (trans.pivot.y * trans.rect.height), trans.localPosition.z); 148 | } 149 | 150 | public static void SetRightTopPosition(this RectTransform trans, Vector2 newPos) 151 | { 152 | trans.localPosition = new Vector3(newPos.x - ((1f - trans.pivot.x) * trans.rect.width), newPos.y - ((1f - trans.pivot.y) * trans.rect.height), trans.localPosition.z); 153 | } 154 | 155 | public static void SetSize(this RectTransform trans, Vector2 newSize) 156 | { 157 | var oldSize = trans.rect.size; 158 | var deltaSize = newSize - oldSize; 159 | trans.offsetMin -= new Vector2(deltaSize.x * trans.pivot.x, deltaSize.y * trans.pivot.y); 160 | trans.offsetMax += new Vector2(deltaSize.x * (1f - trans.pivot.x), deltaSize.y * (1f - trans.pivot.y)); 161 | } 162 | 163 | public static void SetWidth(this RectTransform trans, float newSize) 164 | { 165 | SetSize(trans, new Vector2(newSize, trans.rect.size.y)); 166 | } 167 | 168 | public static void SetHeight(this RectTransform trans, float newSize) 169 | { 170 | SetSize(trans, new Vector2(trans.rect.size.x, newSize)); 171 | } 172 | } -------------------------------------------------------------------------------- /SpeechMod/Unity/Extensions/UIHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using Kingmaker; 4 | using Kingmaker.UI.Common; 5 | using UniRx.Triggers; 6 | using UnityEngine; 7 | using UnityEngine.UI; 8 | 9 | namespace SpeechMod.Unity.Extensions; 10 | 11 | public static class UIHelper 12 | { 13 | public static Color HoverColor = Color.blue; 14 | 15 | public static Coroutine ExecuteLater(this MonoBehaviour behaviour, float delay, Action action) 16 | { 17 | return behaviour.StartCoroutine(InternalExecute(delay, action)); 18 | } 19 | 20 | private static IEnumerator InternalExecute(float delay, Action action) 21 | { 22 | yield return new WaitForSeconds(delay); 23 | action?.Invoke(); 24 | } 25 | 26 | public static void UpdateHoverColor() 27 | { 28 | HoverColor = new Color(Main.Settings.HoverColorR, Main.Settings.HoverColorG, Main.Settings.HoverColorB, Main.Settings.HoverColorA); 29 | } 30 | 31 | public static bool IsParentClickable(this Transform transform) 32 | { 33 | return transform.GetComponentInParents() != null; 34 | } 35 | 36 | public static T GetComponentInParents(this Transform transform) where T : Component 37 | { 38 | var parent = transform?.parent; 39 | while (parent != null && parent != transform.root) 40 | { 41 | var component = parent.GetComponent(); 42 | if (component != null) 43 | { 44 | return component; 45 | } 46 | parent = parent.parent; 47 | } 48 | return null; 49 | } 50 | 51 | public static void SetRaycastTarget(this Graphic graphic, bool enable) 52 | { 53 | if (graphic == null) 54 | return; 55 | 56 | graphic.raycastTarget = enable; 57 | } 58 | 59 | public static Transform TryFind(this Transform transform, string n) 60 | { 61 | if (string.IsNullOrWhiteSpace(n) || transform == null) 62 | return null; 63 | 64 | try 65 | { 66 | return transform.Find(n); 67 | } 68 | catch 69 | { 70 | Debug.Log("TryFind found nothing!"); 71 | } 72 | 73 | return null; 74 | } 75 | 76 | public static Transform TryFind(string n) 77 | { 78 | if (string.IsNullOrWhiteSpace(n)) 79 | return null; 80 | 81 | try 82 | { 83 | return GameObject.Find(n)?.transform; 84 | } 85 | catch 86 | { 87 | Debug.Log("TryFind found nothing!"); 88 | } 89 | 90 | return null; 91 | } 92 | 93 | public static string GetGameObjectPath(this Transform transform) 94 | { 95 | var path = transform?.name; 96 | while (transform?.parent != null) 97 | { 98 | transform = transform.parent; 99 | path = transform.name + "/" + path; 100 | } 101 | return path; 102 | } 103 | public static Transform GetUICanvas() 104 | { 105 | return UIUtility.IsGlobalMap() 106 | ? Game.Instance.UI.GlobalMapUI.transform 107 | : Game.Instance.UI.Canvas.transform; 108 | } 109 | 110 | public static Transform TryFindInStaticCanvas(string n) 111 | { 112 | return TryFindInStaticCanvas(n, n); 113 | } 114 | 115 | public static Transform TryFindInStaticCanvas(string canvasName, string globalMapName) 116 | { 117 | return UIUtility.IsGlobalMap() 118 | ? Game.Instance.UI.GlobalMapUI.transform.TryFind(globalMapName) 119 | : Game.Instance.UI.Canvas.transform.TryFind(canvasName); 120 | } 121 | 122 | public static Transform TryFindInFadeCanvas(string n) 123 | { 124 | return Game.Instance.UI.FadeCanvas.transform.TryFind(n); 125 | } 126 | 127 | /// 128 | /// Returns true if the component was added, false if it already existed. 129 | /// 130 | /// The type of component. 131 | /// The GameObject to add the component to. 132 | /// The component that either was added, or already existed. 133 | /// 134 | public static bool TryAddComponent(this GameObject gameObject, out T component) where T : Component 135 | { 136 | component = gameObject.GetComponent(); 137 | if (component != null) 138 | { 139 | return false; 140 | } 141 | component = gameObject.AddComponent(); 142 | return true; 143 | } 144 | 145 | public static T EnsureComponent(this GameObject gameObject) where T : Component 146 | { 147 | var component = gameObject.GetComponent(); 148 | if (component == null) 149 | { 150 | component = gameObject.AddComponent(); 151 | } 152 | return component; 153 | } 154 | 155 | public static T EnsureComponent(this Transform transform) where T : Component 156 | { 157 | return transform.gameObject.EnsureComponent(); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /SpeechMod/Unity/MenuGUI.cs: -------------------------------------------------------------------------------- 1 | using SpeechMod.Voice; 2 | using System.Linq; 3 | using SpeechMod.Unity.Extensions; 4 | using UnityEngine; 5 | 6 | namespace SpeechMod.Unity; 7 | 8 | public static class MenuGUI 9 | { 10 | private static string m_NarratorPreviewText = "Speech Mod for Pathfinder Wrath of the Righteous - Narrator voice speech test"; 11 | private static string m_FemalePreviewText = "Speech Mod for Pathfinder Wrath of the Righteous - Female voice speech test"; 12 | private static string m_MalePreviewText = "Speech Mod for Pathfinder Wrath of the Righteous - Male voice speech test"; 13 | 14 | public static void OnGui() 15 | { 16 | AddVoiceSelector("Narrator Voice - See nationality below", ref Main.Settings.NarratorVoice, ref m_NarratorPreviewText, ref Main.Settings.NarratorRate, ref Main.Settings.NarratorVolume, ref Main.Settings.NarratorPitch, VoiceType.Narrator); 17 | 18 | GUILayout.BeginVertical("", GUI.skin.box); 19 | 20 | GUILayout.BeginHorizontal(); 21 | GUILayout.Label("Use gender specific voices", GUILayout.ExpandWidth(false)); 22 | Main.Settings.UseGenderSpecificVoices = GUILayout.Toggle(Main.Settings.UseGenderSpecificVoices, "Enabled"); 23 | GUILayout.EndHorizontal(); 24 | 25 | GUILayout.EndVertical(); 26 | 27 | if (Main.Settings.UseGenderSpecificVoices) 28 | { 29 | AddVoiceSelector("Female Voice - See nationality below", ref Main.Settings.FemaleVoice, ref m_FemalePreviewText, ref Main.Settings.FemaleRate, ref Main.Settings.FemaleVolume, ref Main.Settings.FemalePitch, VoiceType.Female); 30 | AddVoiceSelector("Male Voice - See nationality below", ref Main.Settings.MaleVoice, ref m_MalePreviewText, ref Main.Settings.MaleRate, ref Main.Settings.MaleVolume, ref Main.Settings.MalePitch, VoiceType.Male); 31 | } 32 | 33 | GUILayout.BeginVertical("", GUI.skin.box); 34 | 35 | if (Main.Speech is WindowsSpeech) 36 | { 37 | GUILayout.BeginHorizontal(); 38 | GUILayout.Label("Interrupt speech on play", GUILayout.ExpandWidth(false)); 39 | GUILayout.Space(10); 40 | Main.Settings.InterruptPlaybackOnPlay = GUILayout.Toggle(Main.Settings.InterruptPlaybackOnPlay, Main.Settings.InterruptPlaybackOnPlay ? "Interrupt and play" : "Add to queue"); 41 | GUILayout.EndHorizontal(); 42 | } 43 | 44 | GUILayout.BeginHorizontal(); 45 | GUILayout.Label("Auto play dialog", GUILayout.ExpandWidth(false)); 46 | GUILayout.Space(10); 47 | Main.Settings.AutoPlay = GUILayout.Toggle(Main.Settings.AutoPlay, "Enabled"); 48 | GUILayout.EndHorizontal(); 49 | 50 | { 51 | GUI.enabled = Main.Settings.AutoPlay; 52 | 53 | GUILayout.BeginHorizontal(); 54 | GUILayout.Label("Auto play ignores voiced dialog lines", GUILayout.ExpandWidth(false)); 55 | GUILayout.Space(10); 56 | Main.Settings.AutoPlayIgnoreVoice = GUILayout.Toggle(Main.Settings.AutoPlayIgnoreVoice, "Enabled"); 57 | GUILayout.EndHorizontal(); 58 | 59 | GUI.enabled = true; 60 | } 61 | 62 | GUILayout.EndVertical(); 63 | 64 | GUILayout.BeginVertical("", GUI.skin.box); 65 | 66 | GUILayout.BeginHorizontal(); 67 | GUILayout.Label("Show playback button of dialog answers", GUILayout.ExpandWidth(false)); 68 | GUILayout.Space(10); 69 | Main.Settings.ShowPlaybackOfDialogAnswers = GUILayout.Toggle(Main.Settings.ShowPlaybackOfDialogAnswers, "Enabled"); 70 | GUILayout.EndHorizontal(); 71 | 72 | if (Main.Settings.ShowPlaybackOfDialogAnswers) 73 | { 74 | GUILayout.BeginHorizontal(); 75 | GUILayout.Label("Include dialog answer number in playback", GUILayout.ExpandWidth(false)); 76 | GUILayout.Space(10); 77 | Main.Settings.SayDialogAnswerNumber = GUILayout.Toggle(Main.Settings.SayDialogAnswerNumber, "Enabled"); 78 | GUILayout.EndHorizontal(); 79 | 80 | GUILayout.EndVertical(); 81 | 82 | AddColorPicker("Color answer on hover", ref Main.Settings.DialogAnswerColorOnHover, "Hover color", ref Main.Settings.DialogAnswerHoverColorR, ref Main.Settings.DialogAnswerHoverColorG, ref Main.Settings.DialogAnswerHoverColorB, ref Main.Settings.DialogAnswerHoverColorA); 83 | } 84 | else 85 | { 86 | GUILayout.EndVertical(); 87 | } 88 | 89 | AddColorPicker("Color on text hover", ref Main.Settings.ColorOnHover, "Hover color", ref Main.Settings.HoverColorR, ref Main.Settings.HoverColorG, ref Main.Settings.HoverColorB, ref Main.Settings.HoverColorA); 90 | 91 | GUILayout.BeginVertical("", GUI.skin.box); 92 | 93 | GUILayout.BeginHorizontal(); 94 | GUILayout.Label("Font style on text hover", GUILayout.ExpandWidth(false)); 95 | Main.Settings.FontStyleOnHover = GUILayout.Toggle(Main.Settings.FontStyleOnHover, "Enabled"); 96 | GUILayout.EndHorizontal(); 97 | 98 | if (Main.Settings.FontStyleOnHover) 99 | { 100 | GUILayout.BeginHorizontal(); 101 | for (var i = 0; i < Main.Settings.FontStyles.Length; ++i) 102 | { 103 | Main.Settings.FontStyles[i] = GUILayout.Toggle(Main.Settings.FontStyles[i], Main.FontStyleNames[i], GUILayout.ExpandWidth(true)); 104 | } 105 | GUILayout.EndHorizontal(); 106 | } 107 | 108 | GUILayout.EndVertical(); 109 | 110 | GUILayout.BeginVertical("", GUI.skin.box); 111 | 112 | GUILayout.BeginHorizontal(); 113 | GUILayout.Label("Phonetic dictionary", GUILayout.ExpandWidth(false)); 114 | GUILayout.Space(10); 115 | if (GUILayout.Button("Reload", GUILayout.ExpandWidth(false))) 116 | PhoneticDictionary.LoadDictionary(); 117 | GUILayout.EndHorizontal(); 118 | 119 | GUILayout.EndVertical(); 120 | } 121 | 122 | private static void AddVoiceSelector(string label, ref int voice, ref string previewString, ref int rate, ref int volume, ref int pitch, VoiceType type) 123 | { 124 | GUILayout.BeginVertical("", GUI.skin.box); 125 | 126 | GUILayout.BeginHorizontal(); 127 | GUILayout.Label(label, GUILayout.ExpandWidth(false)); 128 | GUILayout.EndHorizontal(); 129 | GUILayout.BeginHorizontal(); 130 | 131 | voice = GUILayout.SelectionGrid(voice, Main.VoicesDict.Select(v => new GUIContent(v.Key, v.Value)).ToArray(), 132 | Main.Speech is WindowsSpeech ? 4 : 5 133 | ); 134 | GUILayout.EndHorizontal(); 135 | 136 | GUILayout.BeginHorizontal(); 137 | GUILayout.Label("Nationality", GUILayout.ExpandWidth(false)); 138 | GUILayout.Space(10); 139 | GUILayout.Label(Main.VoicesDict.ElementAt(voice).Value, GUILayout.ExpandWidth(false)); 140 | GUILayout.EndHorizontal(); 141 | 142 | GUILayout.BeginHorizontal(); 143 | GUILayout.Label("Speech rate", GUILayout.ExpandWidth(false)); 144 | GUILayout.Space(10); 145 | rate = Main.Speech switch 146 | { 147 | WindowsSpeech => (int)GUILayout.HorizontalSlider(rate, -10, 10, GUILayout.Width(300f)), 148 | AppleSpeech => (int)GUILayout.HorizontalSlider(rate, 150, 300, GUILayout.Width(300f)), 149 | _ => rate 150 | }; 151 | GUILayout.Label($" {rate}", GUILayout.ExpandWidth(false)); 152 | GUILayout.EndHorizontal(); 153 | 154 | if (Main.Speech is WindowsSpeech) 155 | { 156 | GUILayout.BeginHorizontal(); 157 | GUILayout.Label("Speech volume", GUILayout.ExpandWidth(false)); 158 | GUILayout.Space(10); 159 | volume = (int)GUILayout.HorizontalSlider(volume, 0, 100, GUILayout.Width(300f)); 160 | GUILayout.Label($" {volume}", GUILayout.ExpandWidth(false)); 161 | GUILayout.EndHorizontal(); 162 | 163 | GUILayout.BeginHorizontal(); 164 | GUILayout.Label("Speech pitch", GUILayout.ExpandWidth(false)); 165 | pitch = (int)GUILayout.HorizontalSlider(pitch, -10, 10, GUILayout.Width(300f)); 166 | GUILayout.Label($" {pitch}", GUILayout.ExpandWidth(false)); 167 | GUILayout.EndHorizontal(); 168 | } 169 | 170 | GUILayout.BeginHorizontal(); 171 | GUILayout.Label("Preivew voice", GUILayout.ExpandWidth(false)); 172 | GUILayout.Space(10); 173 | previewString = GUILayout.TextField(previewString, GUILayout.Width(700f)); 174 | if (GUILayout.Button("Play", GUILayout.ExpandWidth(true))) 175 | Main.Speech.SpeakPreview(previewString, type); 176 | GUILayout.EndHorizontal(); 177 | 178 | GUILayout.EndVertical(); 179 | } 180 | 181 | private static void AddColorPicker(string enableLabel, ref bool enabledBool, string colorLabel, ref float r, ref float g, ref float b, ref float a) 182 | { 183 | GUILayout.BeginVertical("", GUI.skin.box); 184 | GUILayout.BeginHorizontal(); 185 | GUILayout.Label(enableLabel, GUILayout.ExpandWidth(false)); 186 | enabledBool = GUILayout.Toggle(enabledBool, "Enabled"); 187 | GUILayout.EndHorizontal(); 188 | 189 | if (enabledBool) 190 | { 191 | GUILayout.BeginHorizontal(); 192 | GUILayout.Label(colorLabel, GUILayout.ExpandWidth(false)); 193 | GUILayout.Space(10); 194 | GUILayout.Label("R ", GUILayout.ExpandWidth(false)); 195 | r = GUILayout.HorizontalSlider(r, 0, 1); 196 | GUILayout.Space(10); 197 | GUILayout.Label("G", GUILayout.ExpandWidth(false)); 198 | g = GUILayout.HorizontalSlider(g, 0, 1); 199 | GUILayout.Space(10); 200 | GUILayout.Label("B", GUILayout.ExpandWidth(false)); 201 | b = GUILayout.HorizontalSlider(b, 0, 1); 202 | GUILayout.Space(10); 203 | GUILayout.Label("A", GUILayout.ExpandWidth(false)); 204 | a = GUILayout.HorizontalSlider(a, 0, 1); 205 | GUILayout.Space(10); 206 | GUILayout.Box(GetColorPreview(ref r, ref g, ref b, ref a), GUILayout.Width(20)); 207 | GUILayout.EndHorizontal(); 208 | } 209 | GUILayout.EndVertical(); 210 | } 211 | 212 | private static Texture2D GetColorPreview(ref float r, ref float g, ref float b, ref float a) 213 | { 214 | var texture = new Texture2D(20, 20); 215 | for (var y = 0; y < texture.height; y++) 216 | { 217 | for (var x = 0; x < texture.width; x++) 218 | { 219 | texture.SetPixel(x, y, new Color(r, g, b, a)); 220 | } 221 | } 222 | texture.Apply(); 223 | return texture; 224 | } 225 | 226 | public static void UpdateColors() 227 | { 228 | UIHelper.UpdateHoverColor(); 229 | } 230 | } -------------------------------------------------------------------------------- /SpeechMod/Unity/TextMeshProHookData.cs: -------------------------------------------------------------------------------- 1 | using TMPro; 2 | using UniRx; 3 | using UnityEngine; 4 | 5 | namespace SpeechMod.Unity; 6 | 7 | public class TextMeshProHookData : MonoBehaviour 8 | { 9 | public CompositeDisposable Disposables = new(); 10 | 11 | public FontStyles FontStyles { get; set; } 12 | public Color Color { get; set; } 13 | public bool ExtraPadding { get; set; } 14 | 15 | private void OnDestroy() 16 | { 17 | Disposables.Dispose(); 18 | } 19 | } -------------------------------------------------------------------------------- /SpeechMod/Unity/WindowsVoiceUnity.cs: -------------------------------------------------------------------------------- 1 | using SpeechMod.Unity.Extensions; 2 | using System; 3 | using System.Runtime.InteropServices; 4 | using UnityEngine; 5 | 6 | namespace SpeechMod.Unity; 7 | 8 | public class WindowsVoiceUnity : MonoBehaviour 9 | { 10 | public enum WindowsVoiceStatus { Uninitialized, Ready, Speaking, Terminated, Error } 11 | 12 | [DllImport(Constants.WINDOWS_VOICE_DLL)] 13 | private static extern void initSpeech(int rate, int volume); 14 | 15 | [DllImport(Constants.WINDOWS_VOICE_DLL)] 16 | private static extern void destroySpeech(); 17 | 18 | [DllImport(Constants.WINDOWS_VOICE_DLL)] 19 | private static extern void addToSpeechQueue(string s); 20 | 21 | [DllImport(Constants.WINDOWS_VOICE_DLL)] 22 | private static extern void clearSpeechQueue(); 23 | 24 | [DllImport(Constants.WINDOWS_VOICE_DLL)] 25 | [return: MarshalAs(UnmanagedType.BStr)] 26 | private static extern string getStatusMessage(); 27 | 28 | [DllImport(Constants.WINDOWS_VOICE_DLL)] 29 | [return: MarshalAs(UnmanagedType.BStr)] 30 | private static extern string getVoicesAvailable(); 31 | 32 | [DllImport(Constants.WINDOWS_VOICE_DLL)] 33 | private static extern int getWordLength(); 34 | 35 | [DllImport(Constants.WINDOWS_VOICE_DLL)] 36 | private static extern int getWordPosition(); 37 | 38 | [DllImport(Constants.WINDOWS_VOICE_DLL)] 39 | private static extern WindowsVoiceStatus getSpeechState(); 40 | 41 | private static WindowsVoiceUnity m_TheVoice; 42 | private static int m_CurrentWordCount; 43 | 44 | public static bool IsSpeaking => getSpeechState() == WindowsVoiceStatus.Speaking; 45 | public static WindowsVoiceStatus VoiceStatus => getSpeechState(); 46 | 47 | private static void Init() 48 | { 49 | initSpeech(1, 100); 50 | } 51 | private static bool IsVoiceInitialized() 52 | { 53 | if (m_TheVoice != null) 54 | return true; 55 | 56 | Main.Logger.Critical("No voice initialized!"); 57 | return false; 58 | } 59 | 60 | void Start() 61 | { 62 | m_CurrentWordCount = 0; 63 | if (m_TheVoice != null) 64 | { 65 | Destroy(gameObject); 66 | } 67 | else 68 | { 69 | m_TheVoice = this; 70 | Init(); 71 | } 72 | } 73 | 74 | public static string[] GetAvailableVoices() 75 | { 76 | var voicesDelimited = getVoicesAvailable(); 77 | if (string.IsNullOrWhiteSpace(voicesDelimited)) 78 | return []; 79 | var voices = voicesDelimited.Split(['\n'], StringSplitOptions.RemoveEmptyEntries); 80 | return voices; 81 | } 82 | 83 | public static void Speak(string text, int length, float delay = 0f) 84 | { 85 | if (!IsVoiceInitialized()) 86 | return; 87 | 88 | if (Main.Settings.InterruptPlaybackOnPlay && IsSpeaking) 89 | Stop(); 90 | 91 | m_CurrentWordCount = length; 92 | if (delay <= 0f) 93 | addToSpeechQueue(text); 94 | else 95 | m_TheVoice.ExecuteLater(delay, () => Speak(text, length)); 96 | } 97 | 98 | public static string GetStatusMessage() 99 | { 100 | return getStatusMessage(); 101 | } 102 | 103 | public static int WordPosition => getWordPosition(); 104 | 105 | public static int WordCount => m_CurrentWordCount; 106 | 107 | public static int WordLength => getWordLength(); 108 | 109 | public static float GetNormalizedProgress() 110 | { 111 | return 1-(float)(m_CurrentWordCount - getWordPosition()) / m_CurrentWordCount; 112 | } 113 | 114 | public static void Stop() 115 | { 116 | if (!IsVoiceInitialized()) 117 | return; 118 | 119 | if (!IsSpeaking) 120 | return; 121 | 122 | destroySpeech(); 123 | Init(); 124 | } 125 | 126 | public static void ClearQueue() 127 | { 128 | clearSpeechQueue(); 129 | } 130 | 131 | void OnDestroy() 132 | { 133 | if (m_TheVoice != this) 134 | return; 135 | 136 | Debug.Log("Destroying speech"); 137 | destroySpeech(); 138 | Debug.Log("Speech destroyed"); 139 | m_TheVoice = null; 140 | } 141 | } -------------------------------------------------------------------------------- /SpeechMod/Voice/AppleSpeech.cs: -------------------------------------------------------------------------------- 1 | using SpeechMod.Unity; 2 | using System; 3 | using System.Diagnostics; 4 | using System.Text.RegularExpressions; 5 | 6 | namespace SpeechMod.Voice; 7 | 8 | public class AppleSpeech : ISpeech 9 | { 10 | public bool IsSpeaking() 11 | { 12 | return AppleVoiceUnity.IsSpeaking(); 13 | } 14 | 15 | public void SpeakPreview(string text, VoiceType type) 16 | { 17 | if (string.IsNullOrEmpty(text)) 18 | { 19 | Main.Logger?.Warning("No text to speak!"); 20 | return; 21 | } 22 | 23 | switch (type) 24 | { 25 | case VoiceType.Narrator: 26 | text = $"-v {Main.Settings.NarratorVoice} -r {Main.Settings.NarratorRate} {text.Replace("\"", "")}"; 27 | break; 28 | case VoiceType.Female: 29 | text = $"-v {Main.Settings.FemaleVoice} -r {Main.Settings.FemaleRate} {text.Replace("\"", "")}"; 30 | break; 31 | case VoiceType.Male: 32 | text = $"-v {Main.Settings.MaleVoice} -r {Main.Settings.MaleRate} {text.Replace("\"", "")}"; 33 | break; 34 | default: 35 | throw new ArgumentOutOfRangeException(nameof(type), type, null); 36 | } 37 | 38 | AppleVoiceUnity.Speak(text); 39 | } 40 | 41 | public void SpeakDialog(string text, float delay = 0f) 42 | { 43 | if (string.IsNullOrEmpty(text)) 44 | { 45 | Main.Logger?.Warning("No text to speak!"); 46 | return; 47 | } 48 | 49 | if (!Main.Settings.UseGenderSpecificVoices) 50 | { 51 | Speak(text, delay); 52 | return; 53 | } 54 | 55 | text = text.PrepareText(); 56 | AppleVoiceUnity.SpeakDialog(text, delay); 57 | } 58 | 59 | public void SpeakAs(string text, VoiceType voiceType, float delay = 0) 60 | { 61 | SpeakPreview(text, voiceType); 62 | } 63 | 64 | public void Speak(string text, float delay) 65 | { 66 | if (string.IsNullOrEmpty(text)) 67 | { 68 | Main.Logger?.Warning("No text to speak!"); 69 | return; 70 | } 71 | 72 | text = text.PrepareText(); 73 | text = new Regex("<[^>]+>").Replace(text, ""); 74 | text = $"-v {Main.NarratorVoice} -r {Main.Settings.NarratorRate} {text.Replace("\"", "")}"; 75 | AppleVoiceUnity.Speak(text, delay); 76 | } 77 | 78 | public void Stop() 79 | { 80 | AppleVoiceUnity.Stop(); 81 | } 82 | 83 | public string[] GetAvailableVoices() 84 | { 85 | var arguments = "say -v '?' | awk '{\\$3=\\\"\\\"; printf \\\"%s;\\\", \\$1\\\"#\\\"\\$2}' | rev | cut -c 2- | rev"; 86 | var process = new Process 87 | { 88 | StartInfo = new ProcessStartInfo 89 | { 90 | FileName = "/bin/bash", 91 | Arguments = "-c \"" + arguments + "\"", 92 | UseShellExecute = false, 93 | RedirectStandardOutput = true, 94 | RedirectStandardError = true, 95 | CreateNoWindow = true 96 | } 97 | }; 98 | 99 | process.Start(); 100 | var error = process.StandardError.ReadToEnd(); 101 | if (!string.IsNullOrWhiteSpace(error)) 102 | Main.Logger.Error(error); 103 | var text = process.StandardOutput.ReadToEnd(); 104 | process.WaitForExit(); 105 | process.Dispose(); 106 | 107 | return !string.IsNullOrWhiteSpace(text) ? text.Split(new[] { ";" }, StringSplitOptions.RemoveEmptyEntries) : null; 108 | 109 | #if DEBUG 110 | Main.Logger.Warning($"[GetAvailableVoices] {error}"); 111 | #endif 112 | } 113 | 114 | public string GetStatusMessage() 115 | { 116 | return "AppleSpeech ready!"; 117 | } 118 | } -------------------------------------------------------------------------------- /SpeechMod/Voice/Enums.cs: -------------------------------------------------------------------------------- 1 | namespace SpeechMod.Voice; 2 | 3 | public enum VoiceType 4 | { 5 | Narrator, 6 | Female, 7 | Male 8 | } -------------------------------------------------------------------------------- /SpeechMod/Voice/ISpeech.cs: -------------------------------------------------------------------------------- 1 | namespace SpeechMod.Voice; 2 | 3 | public interface ISpeech 4 | { 5 | string GetStatusMessage(); 6 | string[] GetAvailableVoices(); 7 | bool IsSpeaking(); 8 | void SpeakPreview(string text, VoiceType voiceType); 9 | void SpeakDialog(string text, float delay = 0f); 10 | void SpeakAs(string text, VoiceType voiceType, float delay = 0f); 11 | void Speak(string text, float delay = 0f); 12 | void Stop(); 13 | } -------------------------------------------------------------------------------- /SpeechMod/Voice/PhoneticDictionary.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Text.RegularExpressions; 9 | 10 | namespace SpeechMod.Voice; 11 | 12 | public static class PhoneticDictionary 13 | { 14 | private static Dictionary s_PhoneticDictionary = new(); 15 | 16 | private static string SpaceOutDate(string text) 17 | { 18 | var pattern = @"([0-9]{2})\/([0-9]{2})\/([0-9]{4})"; 19 | return Regex.Replace(text, pattern, "$1 / $2 / $3"); 20 | } 21 | 22 | public static string PrepareText(this string text) 23 | { 24 | if (s_PhoneticDictionary == null || !s_PhoneticDictionary.Any()) 25 | LoadDictionary(); 26 | 27 | text = text.ToLower(); 28 | text = text.Replace("\"", ""); 29 | text = text.Replace("\r\n", ". "); 30 | text = text.Replace("\n", ". "); 31 | text = text.Replace("\r", ". "); 32 | text = text.Trim(); 33 | text = SpaceOutDate(text); 34 | 35 | // Regex enabled dictionary 36 | return s_PhoneticDictionary?.Aggregate(text, (current, entry) => Regex.Replace(current, entry.Key, entry.Value)); 37 | } 38 | 39 | public static void LoadDictionary() 40 | { 41 | Main.Logger?.Log("Loading phonetic dictionary..."); 42 | try 43 | { 44 | var file = Path.Combine(Path.GetDirectoryName(Process.GetCurrentProcess().MainModule?.FileName) ?? throw new FileNotFoundException("Path to Pathfinder could not be found!"), @"Mods", @"SpeechMod", @"PhoneticDictionary.json"); 45 | var json = File.ReadAllText(file, Encoding.UTF8); 46 | s_PhoneticDictionary = JsonConvert.DeserializeObject>(json); 47 | } 48 | catch (Exception ex) 49 | { 50 | Main.Logger?.LogException(ex); 51 | } 52 | 53 | #if DEBUG 54 | foreach (var entry in s_PhoneticDictionary) 55 | { 56 | Main.Logger?.Log($"{entry.Key}={entry.Value}"); 57 | } 58 | #endif 59 | } 60 | } -------------------------------------------------------------------------------- /SpeechMod/Voice/SpeechExtensions.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using Debug = UnityEngine.Debug; 3 | 4 | namespace SpeechMod.Voice; 5 | 6 | public static class SpeechExtensions 7 | { 8 | public static void AddUiElements(string name) where T : MonoBehaviour 9 | { 10 | GameObject voiceGameObject = null; 11 | try 12 | { 13 | voiceGameObject = Object.FindObjectOfType()?.gameObject; 14 | } 15 | catch 16 | { 17 | // Ignored 18 | } 19 | 20 | if (voiceGameObject != null) 21 | { 22 | Debug.Log($"{typeof(T).Name} found, returning!"); 23 | return; 24 | } 25 | 26 | Debug.Log($"Adding {typeof(T).Name} SpeechMod UI elements."); 27 | 28 | var windowsVoiceGameObject = new GameObject(name); 29 | windowsVoiceGameObject.AddComponent(); 30 | Object.DontDestroyOnLoad(windowsVoiceGameObject); 31 | } 32 | } -------------------------------------------------------------------------------- /SpeechMod/Voice/WindowsSpeech.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Text.RegularExpressions; 4 | using Kingmaker; 5 | using Kingmaker.Blueprints; 6 | using SpeechMod.Unity; 7 | 8 | #if DEBUG 9 | using System.Reflection; 10 | #endif 11 | 12 | namespace SpeechMod.Voice; 13 | 14 | public class WindowsSpeech : ISpeech 15 | { 16 | private static string NarratorVoice => $""; 17 | private static string NarratorPitch => $""; 18 | private static string NarratorRate => $""; 19 | private static string NarratorVolume => $""; 20 | 21 | private static string FemaleVoice => $""; 22 | private static string FemaleVolume => $""; 23 | private static string FemalePitch => $""; 24 | private static string FemaleRate => $""; 25 | 26 | private static string MaleVoice => $""; 27 | private static string MaleVolume => $""; 28 | private static string MalePitch => $""; 29 | private static string MaleRate => $""; 30 | 31 | public string CombinedNarratorVoiceStart => $"{NarratorVoice}{NarratorPitch}{NarratorRate}{NarratorVolume}"; 32 | public string CombinedFemaleVoiceStart => $"{FemaleVoice}{FemalePitch}{FemaleRate}{FemaleVolume}"; 33 | public string CombinedMaleVoiceStart => $"{MaleVoice}{MalePitch}{MaleRate}{MaleVolume}"; 34 | 35 | public virtual string CombinedDialogVoiceStart 36 | { 37 | get 38 | { 39 | if (Game.Instance?.DialogController?.CurrentSpeaker == null) 40 | return CombinedNarratorVoiceStart; 41 | 42 | return Game.Instance.DialogController.CurrentSpeaker.Gender switch 43 | { 44 | Gender.Female => CombinedFemaleVoiceStart, 45 | Gender.Male => CombinedMaleVoiceStart, 46 | _ => CombinedNarratorVoiceStart 47 | }; 48 | } 49 | } 50 | 51 | public static int Length(string text) 52 | { 53 | if (string.IsNullOrWhiteSpace(text)) 54 | return 0; 55 | 56 | var arr = new[] { "—", "-", "\"" }; 57 | 58 | return arr.Aggregate(text, (current, t) => current.Replace(t, "")).Length; 59 | } 60 | 61 | private string FormatGenderSpecificVoices(string text) 62 | { 63 | text = text.Replace("", $"{CombinedNarratorVoiceStart}"); 64 | text = text.Replace("", $"{CombinedDialogVoiceStart}"); 65 | 66 | if (text.StartsWith("")) 67 | text = text.Remove(0, 8); 68 | else 69 | text = CombinedDialogVoiceStart + text; 70 | 71 | if (text.EndsWith(CombinedDialogVoiceStart)) 72 | text = text.Remove(text.Length - CombinedDialogVoiceStart.Length, CombinedDialogVoiceStart.Length); 73 | 74 | if (!text.EndsWith("")) 75 | text += ""; 76 | return text; 77 | } 78 | 79 | private void SpeakInternal(string text, float delay = 0f) 80 | { 81 | text = "" + text + ""; 82 | //if (Main.Settings?.LogVoicedLines == true) 83 | // UnityEngine.Debug.Log(text); 84 | WindowsVoiceUnity.Speak(text, Length(text), delay); 85 | } 86 | 87 | public bool IsSpeaking() 88 | { 89 | return WindowsVoiceUnity.IsSpeaking; 90 | } 91 | 92 | public void SpeakPreview(string text, VoiceType voiceType) 93 | { 94 | if (string.IsNullOrEmpty(text)) 95 | { 96 | Main.Logger?.Warning("No text to speak!"); 97 | return; 98 | } 99 | 100 | text = text.PrepareText(); 101 | text = new Regex("<[^>]+>").Replace(text, ""); 102 | 103 | switch (voiceType) 104 | { 105 | case VoiceType.Narrator: 106 | text = $"{CombinedNarratorVoiceStart}{text}"; 107 | break; 108 | case VoiceType.Female: 109 | text = $"{CombinedFemaleVoiceStart}{text}"; 110 | break; 111 | case VoiceType.Male: 112 | text = $"{CombinedMaleVoiceStart}{text}"; 113 | break; 114 | default: 115 | throw new ArgumentOutOfRangeException(nameof(voiceType), voiceType, null); 116 | } 117 | 118 | WindowsVoiceUnity.Speak(text, Length(text)); 119 | } 120 | 121 | public string PrepareSpeechText(string text) 122 | { 123 | #if DEBUG 124 | UnityEngine.Debug.Log(text); 125 | #endif 126 | text = new Regex("<[^>]+>").Replace(text, ""); 127 | text = text.PrepareText(); 128 | text = $"{CombinedNarratorVoiceStart}{text}"; 129 | 130 | #if DEBUG 131 | if (Assembly.GetEntryAssembly() == null) 132 | UnityEngine.Debug.Log(text); 133 | #endif 134 | return text; 135 | } 136 | 137 | public string PrepareDialogText(string text) 138 | { 139 | text = text.PrepareText(); 140 | 141 | text = new Regex("]+>]+)?>([^<>]*)").Replace(text, "$2"); 142 | 143 | #if DEBUG 144 | if (Assembly.GetEntryAssembly() == null) 145 | UnityEngine.Debug.Log(text); 146 | #endif 147 | 148 | text = FormatGenderSpecificVoices(text); 149 | 150 | #if DEBUG 151 | if (Assembly.GetEntryAssembly() == null) 152 | UnityEngine.Debug.Log(text); 153 | #endif 154 | 155 | return text; 156 | } 157 | 158 | public void SpeakDialog(string text, float delay = 0f) 159 | { 160 | if (string.IsNullOrEmpty(text)) 161 | { 162 | Main.Logger?.Warning("No text to speak!"); 163 | return; 164 | } 165 | 166 | if (!Main.Settings.UseGenderSpecificVoices) 167 | { 168 | Speak(text, delay); 169 | return; 170 | } 171 | 172 | text = PrepareDialogText(text); 173 | 174 | WindowsVoiceUnity.Speak(text, Length(text), delay); 175 | } 176 | 177 | public void SpeakAs(string text, VoiceType voiceType, float delay = 0) 178 | { 179 | if (string.IsNullOrEmpty(text)) 180 | { 181 | Main.Logger?.Warning("No text to speak!"); 182 | return; 183 | } 184 | 185 | if (!Main.Settings!.UseGenderSpecificVoices) 186 | { 187 | Speak(text, delay); 188 | return; 189 | } 190 | 191 | text = voiceType switch 192 | { 193 | VoiceType.Narrator => $"{CombinedNarratorVoiceStart}{text}", 194 | VoiceType.Female => $"{CombinedFemaleVoiceStart}{text}", 195 | VoiceType.Male => $"{CombinedMaleVoiceStart}{text}", 196 | _ => throw new ArgumentOutOfRangeException(nameof(voiceType), voiceType, null) 197 | }; 198 | 199 | SpeakInternal(text, delay); 200 | } 201 | 202 | public void Speak(string text, float delay = 0f) 203 | { 204 | if (string.IsNullOrEmpty(text)) 205 | { 206 | Main.Logger?.Warning("No text to speak!"); 207 | return; 208 | } 209 | 210 | text = PrepareSpeechText(text); 211 | 212 | WindowsVoiceUnity.Speak(text, Length(text), delay); 213 | } 214 | 215 | public void Stop() 216 | { 217 | WindowsVoiceUnity.Stop(); 218 | } 219 | 220 | public string[] GetAvailableVoices() 221 | { 222 | return WindowsVoiceUnity.GetAvailableVoices(); 223 | } 224 | 225 | public string GetStatusMessage() 226 | { 227 | return WindowsVoiceUnity.GetStatusMessage(); 228 | } 229 | } -------------------------------------------------------------------------------- /SpeechMod/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Test/StringManipulationTests.cs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Osmodium/PathfinderTextToSpeechMod/89be354e3e5a6e7102797090c1c1be8839855578/Test/StringManipulationTests.cs -------------------------------------------------------------------------------- /Test/Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | 7 | false 8 | 9 | Library 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | all 19 | 20 | 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | all 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /Todo.txt: -------------------------------------------------------------------------------- 1 | 🔲 Look into loading screen hint, name and description hookup 2 | LoadingScreenPCView 3 | 4 | 🔲 Look into TooltipBrickPortraitAndNameView that has a title 5 | TooltipBrickPortraitAndNameView -------------------------------------------------------------------------------- /WindowsVoice/WindowsVoice.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using System.Collections; 3 | using System.Runtime.InteropServices; 4 | using System.Text; 5 | 6 | public class WindowsVoice : MonoBehaviour { 7 | [DllImport("WindowsVoice")] 8 | public static extern void initSpeech(); 9 | [DllImport("WindowsVoice")] 10 | public static extern void destroySpeech(); 11 | [DllImport("WindowsVoice")] 12 | public static extern void addToSpeechQueue(string s); 13 | [DllImport("WindowsVoice")] 14 | public static extern void statusMessage(StringBuilder str, int length); 15 | 16 | public static WindowsVoice theVoice = null; 17 | // Use this for initialization 18 | void OnEnable () { 19 | if (theVoice == null) 20 | { 21 | theVoice = this; 22 | initSpeech(); 23 | } 24 | //else 25 | //Destroy(gameObject); 26 | } 27 | public void test() 28 | { 29 | speak("Testing"); 30 | } 31 | public void speak(string msg) { 32 | addToSpeechQueue(msg); 33 | } 34 | void OnDestroy() 35 | { 36 | if (theVoice == this) 37 | { 38 | Debug.Log("Destroying speech"); 39 | destroySpeech(); 40 | Debug.Log("Speech destroyed"); 41 | theVoice = null; 42 | } 43 | } 44 | public static string GetStatusMessage() 45 | { 46 | StringBuilder sb = new StringBuilder(40); 47 | statusMessage(sb, 40); 48 | return sb.ToString(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /WindowsVoice/WindowsVoice.h: -------------------------------------------------------------------------------- 1 | #ifdef DLL_EXPORTS 2 | #define DLL_API __declspec(dllexport) 3 | #else 4 | #define DLL_API __declspec(dllimport) 5 | #endif 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #pragma warning(disable:4996) 13 | #include 14 | #pragma warning(default: 4996) 15 | 16 | using namespace std; 17 | 18 | namespace WindowsVoice { 19 | extern "C" { 20 | DLL_API void __cdecl initSpeech(int rate, int volume); 21 | DLL_API void __cdecl addToSpeechQueue(const char* text); 22 | DLL_API void __cdecl clearSpeechQueue(); 23 | DLL_API void __cdecl destroySpeech(); 24 | DLL_API BSTR __cdecl getStatusMessage(); 25 | DLL_API BSTR __cdecl getVoicesAvailable(); 26 | DLL_API UINT32 __cdecl getWordLength(); 27 | DLL_API UINT32 __cdecl getWordPosition(); 28 | DLL_API UINT32 __cdecl getSpeechState(); 29 | } 30 | 31 | enum class speech_state_enum { uninitialized, ready, speaking, terminated, error }; 32 | 33 | mutex theMutex; 34 | list theSpeechQueue; 35 | thread* theSpeechThread = nullptr; 36 | bool shouldTerminate = false; 37 | wstring theStatusMessage; 38 | ULONG wordLength = 0; 39 | ULONG wordPosition = 0; 40 | speech_state_enum speechState = speech_state_enum::uninitialized; 41 | } -------------------------------------------------------------------------------- /WindowsVoice/WindowsVoice.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31702.278 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WindowsVoice", "WindowsVoice.vcxproj", "{AC8E5BA2-5F13-4C97-A35E-069E01781E85}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|x64 = Debug|x64 11 | Debug|x86 = Debug|x86 12 | Release|x64 = Release|x64 13 | Release|x86 = Release|x86 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {AC8E5BA2-5F13-4C97-A35E-069E01781E85}.Debug|x64.ActiveCfg = Debug|x64 17 | {AC8E5BA2-5F13-4C97-A35E-069E01781E85}.Debug|x64.Build.0 = Debug|x64 18 | {AC8E5BA2-5F13-4C97-A35E-069E01781E85}.Debug|x86.ActiveCfg = Debug|Win32 19 | {AC8E5BA2-5F13-4C97-A35E-069E01781E85}.Debug|x86.Build.0 = Debug|Win32 20 | {AC8E5BA2-5F13-4C97-A35E-069E01781E85}.Release|x64.ActiveCfg = Release|x64 21 | {AC8E5BA2-5F13-4C97-A35E-069E01781E85}.Release|x64.Build.0 = Release|x64 22 | {AC8E5BA2-5F13-4C97-A35E-069E01781E85}.Release|x86.ActiveCfg = Release|Win32 23 | {AC8E5BA2-5F13-4C97-A35E-069E01781E85}.Release|x86.Build.0 = Release|Win32 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {F6D39397-AB07-4969-A70E-011BBED40FC0} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /WindowsVoice/WindowsVoice.vcxproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | Win32 7 | 8 | 9 | Debug 10 | x64 11 | 12 | 13 | Release 14 | Win32 15 | 16 | 17 | Release 18 | x64 19 | 20 | 21 | 22 | {AC8E5BA2-5F13-4C97-A35E-069E01781E85} 23 | WindowsVoice 24 | 25 | 26 | 27 | DynamicLibrary 28 | true 29 | v143 30 | MultiByte 31 | 32 | 33 | DynamicLibrary 34 | true 35 | v143 36 | MultiByte 37 | 38 | 39 | DynamicLibrary 40 | false 41 | v143 42 | true 43 | MultiByte 44 | 45 | 46 | DynamicLibrary 47 | false 48 | v143 49 | true 50 | MultiByte 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | .dll 70 | 71 | 72 | .dll 73 | $(SolutionDir)\libs 74 | 75 | 76 | .dll 77 | 78 | 79 | .dll 80 | $(SolutionDir)\libs\ 81 | C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\VC\Tools\MSVC\14.29.30133\atlmfc\include;$(IncludePath) 82 | 83 | 84 | 85 | Level3 86 | Disabled 87 | true 88 | DLL_EXPORTS;_WINDLL;%(PreprocessorDefinitions) 89 | 90 | 91 | true 92 | 93 | 94 | 95 | 96 | Level3 97 | Disabled 98 | true 99 | DLL_EXPORTS;_WINDLL;%(PreprocessorDefinitions) 100 | 101 | 102 | true 103 | 104 | 105 | 106 | 107 | Level3 108 | MaxSpeed 109 | true 110 | true 111 | true 112 | DLL_EXPORTS;_WINDLL;%(PreprocessorDefinitions) 113 | 114 | 115 | true 116 | true 117 | true 118 | 119 | 120 | 121 | 122 | Level3 123 | MaxSpeed 124 | true 125 | true 126 | true 127 | DLL_EXPORTS;_WINDLL;%(PreprocessorDefinitions) 128 | MultiThreaded 129 | 130 | 131 | true 132 | true 133 | true 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | -------------------------------------------------------------------------------- /WindowsVoice/dllmain.cpp: -------------------------------------------------------------------------------- 1 | #include "pch.h" 2 | #include "WindowsVoice.h" 3 | 4 | namespace WindowsVoice 5 | { 6 | void speechThreadFunc(const int rate, const int volume) 7 | { 8 | if (FAILED(::CoInitializeEx(NULL, COINITBASE_MULTITHREADED))) 9 | { 10 | theStatusMessage = L"Error: Failed to initialize COM for Voice."; 11 | speechState = speech_state_enum::error; 12 | return; 13 | } 14 | 15 | ISpVoice* pVoice = nullptr; 16 | 17 | const HRESULT hr = CoCreateInstance(CLSID_SpVoice, nullptr, CLSCTX_ALL, IID_ISpVoice, reinterpret_cast(&pVoice)); 18 | if (!SUCCEEDED(hr)) 19 | { 20 | const LPSTR pText = 0; 21 | 22 | ::FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,nullptr, hr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), pText, 0, nullptr); 23 | LocalFree(pText); 24 | theStatusMessage = L"Error: Failed to create Voice instance."; 25 | speechState = speech_state_enum::error; 26 | return; 27 | } 28 | 29 | theStatusMessage = L"Speech ready."; 30 | speechState = speech_state_enum::ready; 31 | 32 | pVoice->SetRate(rate); 33 | pVoice->SetVolume(volume); 34 | 35 | SPVOICESTATUS voiceStatus; 36 | const wchar_t* priorText = nullptr; 37 | while (!shouldTerminate) 38 | { 39 | pVoice->GetStatus(&voiceStatus, 0); 40 | if (voiceStatus.dwRunningState == SPRS_IS_SPEAKING) 41 | { 42 | if (priorText == nullptr) 43 | { 44 | theStatusMessage = L"Error: SPRS_IS_SPEAKING but text is NULL"; 45 | speechState = speech_state_enum::error; 46 | } 47 | else 48 | { 49 | theStatusMessage = L"Speaking: "; 50 | theStatusMessage.append(priorText); 51 | speechState = speech_state_enum::speaking; 52 | wordLength = voiceStatus.ulInputWordLen; 53 | wordPosition = voiceStatus.ulInputWordPos; 54 | if (!theSpeechQueue.empty()) 55 | { 56 | theMutex.lock(); 57 | if (lstrcmpW(theSpeechQueue.front(), priorText) == 0) 58 | { 59 | delete[] theSpeechQueue.front(); 60 | theSpeechQueue.pop_front(); 61 | } 62 | theMutex.unlock(); 63 | } 64 | } 65 | } 66 | else 67 | { 68 | theStatusMessage = L"Waiting"; 69 | speechState = speech_state_enum::ready; 70 | if (priorText != nullptr) 71 | { 72 | delete[] priorText; 73 | priorText = nullptr; 74 | } 75 | if (!theSpeechQueue.empty()) 76 | { 77 | theMutex.lock(); 78 | priorText = theSpeechQueue.front(); 79 | theSpeechQueue.pop_front(); 80 | theMutex.unlock(); 81 | pVoice->Speak(priorText, SPF_IS_XML | SPF_ASYNC, nullptr); 82 | } 83 | } 84 | Sleep(50); 85 | } 86 | pVoice->Pause(); 87 | pVoice->Release(); 88 | 89 | theStatusMessage = L"Speech thread terminated."; 90 | speechState = speech_state_enum::terminated; 91 | } 92 | 93 | void addToSpeechQueue(const char* text) 94 | { 95 | if (text) 96 | { 97 | const int len = strlen(text) + 1; 98 | const auto wText = new wchar_t[len]; 99 | 100 | memset(wText, 0, len); 101 | ::MultiByteToWideChar(CP_UTF8, NULL, text, -1, wText, len); 102 | 103 | theMutex.lock(); 104 | theSpeechQueue.push_back(wText); 105 | theMutex.unlock(); 106 | } 107 | } 108 | 109 | void clearSpeechQueue() 110 | { 111 | theMutex.lock(); 112 | theSpeechQueue.clear(); 113 | theMutex.unlock(); 114 | } 115 | 116 | void initSpeech(int rate, int volume) 117 | { 118 | shouldTerminate = false; 119 | if (theSpeechThread != nullptr) 120 | { 121 | theStatusMessage = L"Windows Voice thread already started."; 122 | return; 123 | } 124 | theStatusMessage = L"Starting Windows Voice."; 125 | theSpeechThread = new thread(speechThreadFunc, rate, volume); 126 | } 127 | 128 | void destroySpeech() 129 | { 130 | if (theSpeechThread == nullptr) 131 | { 132 | theStatusMessage = L"Warning: Speach thread already destroyed or not started."; 133 | return; 134 | } 135 | theStatusMessage = L"Destroying speech."; 136 | wordLength = 0; 137 | wordPosition = 0; 138 | shouldTerminate = true; 139 | theSpeechThread->join(); 140 | theSpeechQueue.clear(); 141 | delete theSpeechThread; 142 | theSpeechThread = nullptr; 143 | CoUninitialize(); 144 | theStatusMessage = L"Speech destroyed."; 145 | speechState = speech_state_enum::uninitialized; 146 | } 147 | 148 | BSTR getStatusMessage() 149 | { 150 | if (theStatusMessage.empty()) 151 | { 152 | theStatusMessage = L"WindowsVoice not yet initialized!"; 153 | } 154 | return SysAllocString(theStatusMessage.c_str()); 155 | } 156 | 157 | UINT32 getSpeechState() 158 | { 159 | return static_cast(speechState); 160 | } 161 | 162 | BSTR getVoicesAvailable() 163 | { 164 | wstring voices; 165 | HRESULT hr; 166 | CComPtr cpSpCategory = nullptr; 167 | if (SUCCEEDED(hr = SpGetCategoryFromId(SPCAT_VOICES, &cpSpCategory))) 168 | { 169 | CComPtr cpSpEnumTokens; 170 | if (SUCCEEDED(hr = cpSpCategory->EnumTokens(NULL, NULL, &cpSpEnumTokens))) 171 | { 172 | ULONG vCount; 173 | cpSpEnumTokens->GetCount(&vCount); 174 | CComPtr pSpTok; 175 | for (int i = 0; i < vCount; ++i) 176 | { 177 | cpSpEnumTokens->Next(1, &pSpTok, nullptr); 178 | // try to get the Name attribute first; if failed, get the description 179 | CSpDynamicString voiceName; 180 | CComPtr pAttribs; 181 | hr = pSpTok->OpenKey(SPTOKENKEY_ATTRIBUTES, &pAttribs); 182 | if (SUCCEEDED(hr)) 183 | { 184 | hr = pAttribs->GetStringValue(L"Name", &voiceName); 185 | } 186 | if (FAILED(hr)) 187 | { 188 | hr = SpGetDescription(pSpTok, &voiceName); 189 | } 190 | LANGID langid; 191 | if (SUCCEEDED(hr)) 192 | { 193 | hr = SpGetLanguageFromVoiceToken(pSpTok, &langid); 194 | } 195 | if (SUCCEEDED(hr)) 196 | { 197 | // get the locale name (e.g. "English (United States)") 198 | int size = GetLocaleInfoW(langid, LOCALE_SENGLISHDISPLAYNAME, NULL, 0); 199 | if (size != 0) 200 | { 201 | wchar_t* localeNameBuf = new wchar_t[size]; 202 | GetLocaleInfoW(langid, LOCALE_SENGLISHDISPLAYNAME, localeNameBuf, size); 203 | wstring localeName = localeNameBuf; 204 | delete[] localeNameBuf; 205 | // if the voice has attribute "NaturalVoiceType", consider it "natural" 206 | CSpDynamicString naturalVoiceType; 207 | hr = pAttribs->GetStringValue(L"NaturalVoiceType", &naturalVoiceType); 208 | if (SUCCEEDED(hr)) 209 | { 210 | // inserts "Natural" before '(' 211 | size_t pos = localeName.find(L'('); 212 | if (pos != wstring::npos) 213 | localeName.insert(pos, L"Natural "); 214 | else 215 | localeName.append(L" Natural"); 216 | } 217 | voices += voiceName; 218 | voices += L'#'; 219 | voices += localeName; 220 | voices += L'\n'; 221 | } 222 | } 223 | pSpTok.Release(); 224 | } 225 | } 226 | } 227 | return SysAllocString(voices.c_str()); 228 | } 229 | 230 | UINT32 getWordLength() 231 | { 232 | return wordLength; 233 | } 234 | 235 | UINT32 getWordPosition() 236 | { 237 | return wordPosition; 238 | } 239 | } 240 | 241 | BOOL APIENTRY DllMain(HMODULE, DWORD ul_reason_for_call, LPVOID) 242 | { 243 | switch (ul_reason_for_call) 244 | { 245 | case DLL_PROCESS_ATTACH: 246 | case DLL_THREAD_ATTACH: 247 | case DLL_THREAD_DETACH: 248 | case DLL_PROCESS_DETACH: 249 | break; 250 | } 251 | 252 | return TRUE; 253 | } -------------------------------------------------------------------------------- /WindowsVoice/pch.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // pch.cpp 3 | // Include the standard header and generate the precompiled header. 4 | // 5 | 6 | #include "pch.h" 7 | -------------------------------------------------------------------------------- /WindowsVoice/pch.h: -------------------------------------------------------------------------------- 1 | // 2 | // pch.h 3 | // Header for standard system include files. 4 | // 5 | 6 | #pragma once 7 | 8 | #ifndef WIN32_LEAN_AND_MEAN 9 | #define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers 10 | #endif 11 | 12 | // Windows Header Files: 13 | #include 14 | --------------------------------------------------------------------------------