diff --git a/TheForceEngine/Captions/README.txt b/TheForceEngine/Captions/README.txt index fcbdf7fe8..5dc92a0a9 100644 --- a/TheForceEngine/Captions/README.txt +++ b/TheForceEngine/Captions/README.txt @@ -15,6 +15,8 @@ For example, This corresponds to General Moc's third line of dialog during the cutscene before Talay. Because this is for a cutscene, the file name (m01moc03) has no extension. The number at the end indicates that the subtitle should display for 1.5 seconds. +When the duration value is not included, the caption duration is automatically +calculated from the length of the caption (in bytes). ransto01.voc "Trooper: There he is, stop him!" @@ -36,4 +38,14 @@ frequently and are obnoxious if captioned. Lines that begin with // or # are ignored //this is a comment - # this is also a comment \ No newline at end of file + # this is also a comment + +UTF-8 Unicode is supported, but make sure that you select a font which supports the +necessary character set. If a unicode character is missing from the selected font, +the missing character will be replaced by a rectangle or question mark. + +Because automatic caption durations are calculated by measuring the length of the +caption in bytes, and UTF-8 characters may be more than 1 byte long, the automatic +duration may be too long when the caption consists primarily of multi-byte Unicode +characters. You can avoid this issue by including the duration value in the caption +file. \ No newline at end of file diff --git a/TheForceEngine/Captions/subtitles.txt b/TheForceEngine/Captions/subtitles-en.txt similarity index 80% rename from TheForceEngine/Captions/subtitles.txt rename to TheForceEngine/Captions/subtitles-en.txt index 68eb3c378..82d8f0663 100644 --- a/TheForceEngine/Captions/subtitles.txt +++ b/TheForceEngine/Captions/subtitles-en.txt @@ -1,3 +1,5 @@ +example_cutscene "This is an example cutscene caption." + //ENEMIES ranofc02.voc "Officer: Stop where you are!" ranofc04.voc "Officer: You're not authorized in this area." @@ -15,13 +17,9 @@ icmdo-1.voc "Commando: He's over here! Stop that man!" //unused? ioffic-1.voc "Officer: Halt! Hold it right there." m16moc01.voc "General Mohc: It's been a long time since I've challenged a man to battle. I'm glad my opponent is so worthy." + //SFX -boba-1.voc "[Fett laughs]" -boba-3.voc "[Fett yells]" -boba-4.voc "[Fett screams]" -beep-10.voc "[MINE BEEPING]" -fall.voc "[Kyle screams]" -door.voc "[Door hisses]" +door.voc "[Door hisses]" door-04.voc "[Door clanking]" door1-1.voc "[Door hisses]" //door1-2.voc "[Door mechanism whirring]" //too frequent @@ -32,24 +30,41 @@ door2-1.voc "[Door clanking]" door3-1.voc "[Door clanking]" //door3-2.voc "[Door mechanism whirring]" //too frequent //door3-3.voc "[Door thuds]" -ex-lrg1.voc "[Explosion]" -ex-med1.voc "[Big Explosion]" -ex-tiny.voc "[Bang]" -swim-in.voc "[Splash]" -gamor-3.voc "[Porcine grunting]" -reeyee1.voc "[Alien yelling]" -reeyee-1.voc "[Alien yelling]" -probe-1.voc "[Staccato droid speech]" -//probalm.voc "[Circuits overloading]" -bossk-1.voc "[Alien hissing]" -choke.voc "[Kyle gasps and grunts]" 5 -kell-1.voc "[Kell dragon roaring]" -kell-2.voc "[Aggressive growling]" +boba-1.voc "[Fett laughs]" //Boba Fett taunt +//boba-3.voc "[Fett yells]" //Boba Fett pain (nearly inaudible) +boba-4.voc "[Fett screams]" //Boba Fett death +gamor-1.voc "[Porcine scream]" //Gamorrean death +gamor-3.voc "[Porcine grunting]" //Gamorrean alert +reeyee1.voc "[Alien yelling]" //Gran alert +reeyee-1.voc "[Alien yelling]" //Gran alert (duplicate) +reeyee3.voc "[Alien death yell]" //Gran death +reeyee-3.voc "[Alien death yell]" //Gran death (duplicate) +probe-1.voc "[Staccato droid speech]" //Probe alert +probalm.voc "[Circuits overloading]" //Probe death +bossk-1.voc "[Reptilian hissing]" //Trandoshan alert +bosskdie.voc "[Reptilian hiss-scream]" //Trandoshan death +kell-1.voc "[Kell dragon roaring]" //Kell dragon +kell-2.voc "[Aggressive growling]" //Kell dragon +kell-7.voc "[Kell dragon screaming]" //Kell dragon death +intalert.voc "[Warbling droid repulsorlift]" //Interrogator droid alert +weld-die.voc "[Welder grinds to a halt]" //Welder death +st-die-1.voc "[Death yell]" //Trooper/officer death +phase1a.voc "[Menacing electronic buzz]" //Phase 1 alert +phase1c.voc "[Mechanical death rattle]" //Phase 1 death +phase2c.voc "[Synthesized yell, collapsing metal]" //Phase 3 death +phase3a.voc "[Clanking, Mohc laughs]" //Phase 3 taunt +phase3c.voc "[Mohc screams]" //Phase 3 death +ex-lrg1.voc "[Explosion]" +ex-med1.voc "[Big Explosion]" +ex-tiny.voc "[Bang]" +swim-in.voc "[Splash]" +beep-10.voc "[MINE BEEPING]" +fall.voc "[Kyle screams]" +choke.voc "[Kyle gasps and grunts]" 5 landing1.voc "[Thrusters firing]" locked-1.voc "[Locked]" ex-small.voc "[Explosion]" creatur1.voc "[Menacing rumble, water sloshing]" -intalert.voc "[Warbling droid repulsorlift]" //----- MISSION VOICEOVER -------------------------------------- //M01 (Secret Base) @@ -92,11 +107,15 @@ m09jana1.voc "Jan: Those must be smuggler routes to the Arc Hammer. I think it's m10jan01.voc "Jan: Thanks, I thought I was done for." m10kyl03.voc "Kyle: No time for hugs. Let's get out of here." //M11 (Imperial City) +m11kyl01.voc "Kyle: Jan, I've cracked the central lock. I'm in." m11jan01.voc "Jan: Good job Kyle, but you're not done yet." +m11kyl02.voc "Kyle: Navacard inserted and decoding." m11jan02.voc "Jan: Beautiful Kyle. Now get that data tape and high-tail your mercenary hide back to the landing pad. I can't stay out here too long before Imperial security takes notice." 8 +m11kyl03.voc "Kyle: Data tapes in hand, I'm on my way out." m11jan03.voc "Jan: Kyle, something strange is going down over here. Get back here - I mean it!" m11jan04.voc "Jan: Oh no! Kyle, you better look out, I just saw... [loud static]" m11jan05.voc "Jan: Kyle, where are you? I'm back at the landing pad." +m11kyl04.voc "Kyle: Where were you, Jan?" m11jan06.voc "Jan: I had TIE Fighters all over me. I had to properly dispose of them." m11jana6.voc "Jan: I had TIE Fighters all over me. I had to properly dispose of them." //alt. take //M12 (Fuel Station) @@ -118,11 +137,11 @@ m16kyl06.voc "Kyle: For freedom." //----- CUTSCENES ----------------------------------------------- //Before M02 (Talay) m01nar01 "Kyle delivers the plans to the Rebel Alliance. Soon afterwards, the Death Star is destroyed. But even as the Alliance celebrates this victory, another sinister plot is set in motion that will become an even greater concern for the Rebellion." 13.5 -m01vdr01 "The Emperor has approved your test demonstration, General Mohc." -m01moc0a "Thank you Lord Vader. What I unveil today will mark a new era for the Empire." -m01moc01 "Thank you Lord Vader. What I unveil today will mark a new era for the Empire." //duplicate -m01moc0b "We will be able to decimate the Rebels just as we did the Jedi Knights. At last, the Emperor's war will be filled only with the glory and beauty of decisive victory." -m01vdr02 "A noble cause, General. I hope the demonstration lives up to your claims. Proceed." +m01vdr01 "The Emperor has approved your test demonstration, General Mohc." 4 +m01moc0a "Thank you Lord Vader. What I unveil today will mark a new era for the Empire." 5.1 +m01moc01 "Thank you Lord Vader. What I unveil today will mark a new era for the Empire." 5.1 //duplicate +m01moc0b "We will be able to decimate the Rebels just as we did the Jedi Knights. At last, the Emperor's war will be filled only with the glory and beauty of decisive victory." 11 +m01vdr02 "A noble cause, General. I hope the demonstration lives up to your claims. Proceed." 6 m01moc02 "With pleasure." m01moc03 "Dark Trooper: release." intcom3 "[Indistinct PA chatter]" @@ -133,7 +152,7 @@ beep-3 "[Loud beep]" dt-door2 "[Mechanical noises]" dt-lower "[Equipment whirring]" dtlaunch "[Engine blast]" -dive1 "[Howling engines]" +dive1 "[Howling engines]" m01vdr03 "Very impressive, General. The Emperor will be most pleased. Continue with your project." m01moc04 "Certainly, Lord Vader." m01mma01 "Thank you commander, for responding at such short notice. The Empire has been keeping us on the run since the destruction of the Death Star." @@ -157,13 +176,13 @@ m05moc02 "I understand the threat, Lord Vader. Katarn was once an impressive Imp guncock "[Blaster cocking]" //Before M10 (Jabba's Ship) -ray "[Beam pulsing]" 2.9 +ray "[Beam pulsing]" 2.9 hyp-in-8 "[Engines whining]" 3 shiplock "[Clang]" -snort "[Gamorreans grunting]" +snort "[Gamorreans grunting]" m10kyl01 "Jabba? What have you done with Jan? If any harm comes to her, I'll personally shove my blaster down your slimy throat." m10kyl02 "I wish you were here too, Jabba. There's nothing like roast Kell dragon." -pigpush "[Gamorrean grunts]" +pigpush "[Gamorrean grunts]" //M16 (unused?) m16moc02 "You are an excellent adversary, commander. The warrior's flame burns in your soul. It is a shame that we are not allies." @@ -179,7 +198,7 @@ m16vdr01 "This is an unfortunate setback. The Force is strong with Katarn..." 4. m12imp01 "Smuggler ship, your flight path is clear. Begin your docking procedure." 4.5 xw-nf-1 "[Engines roaring]" ex-grom1 "[Distant booming]" 4 -tube1 "[Mechanisms whirring]" +tube1 "[Mechanisms whirring]" //FOR TESTING //jump-1.voc "[Jump]" diff --git a/TheForceEngine/Documentation/markdown/TheForceEngineManual.md b/TheForceEngine/Documentation/markdown/TheForceEngineManual.md index a8c429d98..111499d74 100644 --- a/TheForceEngine/Documentation/markdown/TheForceEngineManual.md +++ b/TheForceEngine/Documentation/markdown/TheForceEngineManual.md @@ -38,3 +38,14 @@ Open the built-in editors. Note that the editors are currently disabled due to l **Exit** Exit the application. + +## Accessibility Settings +The Accessibility Settings menu includes customization settings for closed captions/subtitles. You can separately enable or disable voice subtitles (e.g. "You there, stop where you are!") and captions (e.g. "[Mechanical death rattle]") for gameplay and cutscenes. +### Custom captions and fonts +It is possible to add custom caption files and fonts, for example to support subtitles in other languages. + +**Custom caption files** +Custom caption files can be added to the `Captions` directory in your TFE install directory (e.g. **`C:\TheForceEngine\Captions`**) or in the TFE documents directory (e.g. **`C:\Users\UserName\Documents\TheForceEngine\Captions`** on Windows). The **`Captions`** directory in the TFE install directory includes a **`README.txt`** file with notes about the caption format. UTF-8 Unicode is supported, but make sure that you select a font which supports the necessary character set. + +**Custom fonts** +Custom fonts in TrueType (.ttf) format can be added to the **`Fonts`** directory in your TFE install directory (e.g. **`C:\TheForceEngine\Fonts`**) or in the TFE documents directory (e.g. **`C:\Users\UserName\Documents\TheForceEngine\Fonts`** on Windows). Unicode character sets are supported. \ No newline at end of file diff --git a/TheForceEngine/Fonts/NotoSans-Regular.ttf b/TheForceEngine/Fonts/NotoSans-Regular.ttf new file mode 100644 index 000000000..7552fbe80 Binary files /dev/null and b/TheForceEngine/Fonts/NotoSans-Regular.ttf differ diff --git a/TheForceEngine/Fonts/OFL (Noto Sans).txt b/TheForceEngine/Fonts/OFL (Noto Sans).txt new file mode 100644 index 000000000..0c4ce67bd --- /dev/null +++ b/TheForceEngine/Fonts/OFL (Noto Sans).txt @@ -0,0 +1,93 @@ +Copyright 2015-2021 Google LLC. All Rights Reserved. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/TheForceEngine/TFE_A11y/accessibility.cpp b/TheForceEngine/TFE_A11y/accessibility.cpp index 0ecb829af..683646406 100644 --- a/TheForceEngine/TFE_A11y/accessibility.cpp +++ b/TheForceEngine/TFE_A11y/accessibility.cpp @@ -1,16 +1,20 @@ #include #include #include -#include #include "accessibility.h" +#include #include +#include #include #include #include #include #include #include +#include +#include +#include using namespace std::chrono; using std::string; @@ -20,18 +24,28 @@ namespace TFE_A11Y // a11y is industry slang for accessibility /////////////////////////////////////////// // Forward Declarations /////////////////////////////////////////// - void addCaption(const ConsoleArgList& args); + void findFontFiles(); + bool isFontLoaded(); + void loadDefaultFont(bool clearAtlas); + void tryLoadFont(const string path, bool clearAtlas); + void loadFont(const string path, bool clearAtlas); + void findCaptionFiles(); + bool filterCaptionFile(const string fileName); + void onFileError(const string path); + void enqueueCaption(const ConsoleArgList& args); void drawCaptions(std::vector* captions); string toUpper(string input); string toLower(string input); + string toFileName(string language); void loadScreenSize(); ImVec2 calcWindowSize(f32* fontScale, CaptionEnv env); s64 secondsToMicroseconds(f32 seconds); - s64 calculateDuration(string text); + s64 calculateDuration(const string text); /////////////////////////////////////////// // Constants /////////////////////////////////////////// + const char* DEFAULT_FONT = "Fonts/NotoSans-Regular.ttf"; const f32 MAX_CAPTION_WIDTH = 1200; const f32 DEFAULT_LINE_HEIGHT = 20; const f32 LINE_PADDING = 5; @@ -51,11 +65,17 @@ namespace TFE_A11Y // a11y is industry slang for accessibility /////////////////////////////////////////// // Static vars /////////////////////////////////////////// + static A11yStatus s_status = CC_NOT_LOADED; + static FilePathList s_captionFileList; + static FilePathList s_fontFileList; + static FilePath s_currentCaptionFile; + static FilePath s_currentFontFile; static s64 s_maxDuration = secondsToMicroseconds(10.0f); static DisplayInfo s_display; static u32 s_screenWidth; static u32 s_screenHeight; static bool s_active = true; + static bool s_logSFXNames = false; static system_clock::duration s_lastTime; static FileStream s_captionsStream; @@ -64,21 +84,237 @@ namespace TFE_A11Y // a11y is industry slang for accessibility static std::map s_captionMap; static std::vector s_activeCaptions; static std::vector s_exampleCaptions; + static std::map s_fontMap; + static ImFont* s_currentCaptionFont; + static string s_pendingFontPath; + // Initialize the Accessibility system. Only call this once on application launch. void init() { - CCMD("showCaption", addCaption, 1, "Display a test caption. Example: showCaption \"Hello, world\""); + assert(s_status == CC_NOT_LOADED); + CCMD("showCaption", enqueueCaption, 1, "Display a test caption. Example: showCaption \"Hello, world\""); + CVAR_BOOL(s_logSFXNames, "d_logSFXNames", CVFLAG_DO_NOT_SERIALIZE, "If enabled, log the name of each sound effect that plays."); - s_captionsStream.open("Captions/subtitles.txt", Stream::AccessMode::MODE_READ); + findCaptionFiles(); + findFontFiles(); + } + + ////////////////////////////////////////////////////// + // Fonts + ////////////////////////////////////////////////////// + + // Get the list of all font files we detect in the Fonts directories. + FilePathList getFontFiles() { return s_fontFileList; } + + // The name and path of the currently selected Font file + FilePath getCurrentFontFile() { return s_currentFontFile; } + + // True if we are waiting to load a font after we render ImGui. + bool hasPendingFont() { return !s_pendingFontPath.empty(); } + + bool isFontLoaded() { return s_currentCaptionFont != nullptr && s_currentCaptionFont->IsLoaded(); } + + const ImWchar* GetGlyphRanges() + { + static const ImWchar ranges[] = + { + 0x0020, 0xEEFF, // All glyphs in font + 0, + }; + return &ranges[0]; + } + + // Get all font file names from the Fonts directories; we will use this to populate the + // dropdown in the Accessibility settings menu. + void findFontFiles() + { + // First we check the User Documents directory for custom font files added by the user. + char docsFontsDir[TFE_MAX_PATH]; + const char* docsDir = TFE_Paths::getPath(PATH_USER_DOCUMENTS); + sprintf(docsFontsDir, "%sFonts/", docsDir); + if (!FileUtil::directoryExits(docsFontsDir)) + { + FileUtil::makeDirectory(docsFontsDir); + } + s_fontFileList.addFiles(docsFontsDir, "ttf", nullptr); + + // Then we check the Program directory for font files that shipped with TFE. + char programFontsDir[TFE_MAX_PATH]; + sprintf(programFontsDir, "Fonts/"); + TFE_Paths::mapSystemPath(programFontsDir); + s_fontFileList.addFiles(programFontsDir, "ttf", nullptr); + + // Try to load the previously selected font. + string lastFontPath = TFE_Settings::getA11ySettings()->lastFontPath; + tryLoadFont(lastFontPath, false); + } + + // Specify a font to load after ImGui finishes rendering. + void setPendingFont(const string path) + { + s_pendingFontPath = path; + } + + void loadDefaultFont(bool clearAtlas) + { + char fontpath[TFE_MAX_PATH]; + sprintf(fontpath, DEFAULT_FONT); + TFE_Paths::mapSystemPath(fontpath); + loadFont(fontpath, clearAtlas); + } + + // Try to load the font at the given path. If the font doesn't exist or can't be read, + // we automatically fall back to the default font. + void tryLoadFont(const string path, bool clearAtlas) + { + if (!path.empty() && FileUtil::exists(path.c_str())) + { + try + { + loadFont(path, clearAtlas); + } + catch (...) + { + TFE_System::logWrite(LOG_ERROR, "a11y", string("Couldn't read font file at " + path + + "; falling back to default font").c_str()); + loadDefaultFont(clearAtlas); + } + } + else + { + TFE_System::logWrite(LOG_ERROR, "a11y", string("Couldn't find font file at " + path + + "; falling back to default font").c_str()); + loadDefaultFont(clearAtlas); + } + } + + // Load the font at the given path. The font will be added to the ImGui font atlas if it hasn't + // been loaded before. + void loadFont(const string path, bool clearAtlas) + { + ImGuiIO& io = ImGui::GetIO(); + + if (s_fontMap.count(path) > 0) // Font has already been loaded. + { + s_currentCaptionFont = s_fontMap.at(path); + } + else // Font hasn't been loaded before. + { + s_currentCaptionFont = io.Fonts->AddFontFromFileTTF(path.c_str(), 32, nullptr, GetGlyphRanges()); + s_fontMap[path] = s_currentCaptionFont; + // If we're loading a new font after the game first initializes, we'll need to clear the + // ImGui font atlas so that it is automatically regenerated at the start of the next frame. + if (clearAtlas) { ImGui_ImplOpenGL3_DestroyFontsTexture(); } + } + assert(s_currentCaptionFont != nullptr); + + char name[256]; + FileUtil::getFileNameFromPath(path.c_str(), name); + s_currentFontFile.name = string(name); + s_currentFontFile.path = path; + + TFE_Settings::getA11ySettings()->lastFontPath = path; + } + + void loadPendingFont() + { + tryLoadFont(s_pendingFontPath, true); + s_pendingFontPath.clear(); + } + + ////////////////////////////////////////////////////// + // Captions + ////////////////////////////////////////////////////// + + // Get the list of all caption files we detect in the Captions directories. + FilePathList getCaptionFiles() { return s_captionFileList; } + + // The name and path of the currently selected Caption file + FilePath getCurrentCaptionFile() { return s_currentCaptionFile; } + + A11yStatus getStatus() { return s_status; } + + // Get all caption file names from the Captions directories; we will use this to populate the + // dropdown in the Accessibility settings menu. + void findCaptionFiles() + { + // First we check the User Documents directory for custom subtitle files added by the user. + char docsCaptionsDir[TFE_MAX_PATH]; + const char* docsDir = TFE_Paths::getPath(PATH_USER_DOCUMENTS); + sprintf(docsCaptionsDir, "%sCaptions/", docsDir); + if (!FileUtil::directoryExits(docsCaptionsDir)) + { + FileUtil::makeDirectory(docsCaptionsDir); + } + s_captionFileList.addFiles(docsCaptionsDir, "txt", filterCaptionFile); + + // Then we check the Program directory for subtitle files that shipped with TFE. + char programCaptionsDir[TFE_MAX_PATH]; + const char* programDir = TFE_Paths::getPath(PATH_PROGRAM); + sprintf(programCaptionsDir, "%sCaptions/", programDir); + s_captionFileList.addFiles(programCaptionsDir, "txt", filterCaptionFile); + + // Try to load captions for the previously selected language. + string search = toFileName(TFE_Settings::getA11ySettings()->language); + vector* captionFilePaths = s_captionFileList.getFilePaths(); + for (size_t i = 0; i < captionFilePaths->size(); i++) + { + string path = captionFilePaths->at(i); + if (path.find(search) != string::npos) { + loadCaptions(path); + break; + } + } + + // If the language didn't load, default to English. + if (s_status != CC_LOADED) + { + string fileName = programCaptionsDir + toFileName("en"); + loadCaptions(fileName); + } + } + + bool filterCaptionFile(const string fileName) + { + return fileName.substr(0, FILE_NAME_START.length()) == FILE_NAME_START; + } + + // Load the caption file with the given name and parse the subs/captions from it + void loadCaptions(const string path) + { + s_captionMap.clear(); + + // Try to open the file + s_currentCaptionFile.path = path; + if (!s_captionsStream.open(path.c_str(), Stream::AccessMode::MODE_READ)) + { + onFileError(path); + return; + } + + // Parse language name; for example, if file name is "subtitles-de.txt", the language is "de". + // The idea is for the language name to be an ISO 639-1 two-letter code, but for now the system + // doesn't actually care how long the language name is or whether it's a valid 639-1 code. + size_t start = path.find(FILE_NAME_START); + if (start != string::npos) + { + s_currentCaptionFile.name = path.substr(start); + string language = path.substr(start + FILE_NAME_START.length()); + language = language.substr(0, language.length() - FILE_NAME_EXT.length()); + TFE_Settings::getA11ySettings()->language = language; + } + + // Read file into buffer. auto size = (u32)s_captionsStream.getSize(); s_captionsBuffer = (char*)malloc(size); s_captionsStream.readBuffer(s_captionsBuffer, size); + // Init parser (configured to ignore comment lines). s_parser.init(s_captionsBuffer, size); - // Ignore commented lines s_parser.addCommentString("#"); s_parser.addCommentString("//"); + // Parse each line from the caption file. size_t bufferPos = 0; while (bufferPos < size) { @@ -92,7 +328,7 @@ namespace TFE_A11Y // a11y is industry slang for accessibility Caption caption = Caption(); caption.text = tokens[1]; - // Optional third field is duration in seconds, mainly useful for cutscenes + // Optional third field is duration in seconds, mainly useful for cutscenes. if (tokens.size() > 2) { try @@ -104,49 +340,60 @@ namespace TFE_A11Y // a11y is industry slang for accessibility } if (caption.microsecondsRemaining <= 0) { - // Calculate caption duration based on text length + // Calculate caption duration based on text length. caption.microsecondsRemaining = calculateDuration(caption.text); if (caption.microsecondsRemaining > s_maxDuration) caption.microsecondsRemaining = (s64)s_maxDuration; } assert(caption.microsecondsRemaining > 0); - if (caption.text[0] == '[') caption.type = CC_Effect; - else caption.type = CC_Voice; + if (caption.text[0] == '[') { caption.type = CC_EFFECT; } + else { caption.type = CC_VOICE; } string name = toLower(tokens[0]); s_captionMap[name] = caption; }; + + s_status = CC_LOADED; + delete s_captionsBuffer; + } + + void onFileError(const string path) + { + string error = "Couldn't find caption file at " + path; + TFE_System::logWrite(LOG_ERROR, "a11y", error.c_str()); + s_status = CC_ERROR; + // TODO: display an error dialog } - void clearCaptions() + void clearActiveCaptions() { s_activeCaptions.clear(); + s_exampleCaptions.clear(); } void onSoundPlay(char* name, CaptionEnv env) { const TFE_Settings_A11y* settings = TFE_Settings::getA11ySettings(); // Don't add caption if captions are disabled for the current env - if (env == CC_Cutscene && !settings->showCutsceneCaptions && !settings->showCutsceneSubtitles) { return; } - if (env == CC_Gameplay && !settings->showGameplayCaptions && !settings->showGameplaySubtitles) { return; } + if (env == CC_CUTSCENE && !settings->showCutsceneCaptions && !settings->showCutsceneSubtitles) { return; } + if (env == CC_GAMEPLAY && !settings->showGameplayCaptions && !settings->showGameplaySubtitles) { return; } - // TODO: enable/disable this line of logging with console variable - //TFE_System::logWrite(LOG_ERROR, "a11y", name); + if (s_logSFXNames) { TFE_System::logWrite(LOG_ERROR, "a11y", name); } string nameLower = toLower(name); if (s_captionMap.count(nameLower)) { - Caption caption = s_captionMap[nameLower]; //copy + Caption caption = s_captionMap[nameLower]; // Copy caption.env = env; // Don't add caption if the last caption has the same text if (s_activeCaptions.size() > 0 && s_activeCaptions.back().text == caption.text) { return; } - if (env == CC_Cutscene) + if (env == CC_CUTSCENE) { // Don't add caption if this type of caption is disabled - if (caption.type == CC_Effect && !settings->showCutsceneCaptions) return; - else if (caption.type == CC_Voice && !settings->showCutsceneSubtitles) return; + if (caption.type == CC_EFFECT && !settings->showCutsceneCaptions) { return; } + else if (caption.type == CC_VOICE && !settings->showCutsceneSubtitles) { return; } s32 maxLines = CUTSCENE_MAX_LINES[settings->cutsceneFontSize]; @@ -155,58 +402,87 @@ namespace TFE_A11Y // a11y is industry slang for accessibility loadScreenSize(); f32 fontScale; auto windowSize = calcWindowSize(&fontScale, env); - const f32 CHAR_WIDTH = 10.12f; assert(fontScale > 0); - s32 maxCharsPerLine = (s32)(windowSize.x / (CHAR_WIDTH * fontScale)); - s32 maxChars = maxCharsPerLine * maxLines; s32 count = 1; - while (caption.text.length() > maxChars) + size_t idx = 0; + string chunk; + s32 chunkLineCount = 0; + string line = ""; + while (idx < caption.text.length()) { - Caption next = caption; // Copy - s32 spaceIndex = 0; - for (s32 i = 0; i < maxLines; i++) + // Extend the line one character at a time until we exceed the width of the panel + // or run out of text. + line += caption.text.at(idx); + idx++; + ImGui::PushFont(s_currentCaptionFont); + auto textSize = ImGui::CalcTextSize(line.c_str(), 0, false, -1.0f); + ImGui::PopFont(); + // If we exceed the width of the panel... + if (textSize.x * fontScale > windowSize.x * 0.95f) { - spaceIndex = (s32)next.text.rfind(' ', spaceIndex + maxCharsPerLine); - if (spaceIndex < 0) { break; } + size_t spaceIndex = line.rfind(' '); + // ...and the line has a space in it, add the line to the chunk. + if (spaceIndex > 0) + { + chunk += line.substr(0, spaceIndex) + "\n"; + idx -= (line.length() - spaceIndex - 1); + line = string(""); + chunkLineCount++; + } + else { break; } + + // If the chunk has three lines, add it as a new caption. + if (chunkLineCount >= 3) + { + s32 length = chunk.length(); + f32 ratio = length / (f32)caption.text.length(); + Caption next = caption; // Copy + next.text = chunk; + next.microsecondsRemaining = s64(next.microsecondsRemaining * ratio); + enqueueCaption(next); + + // Start a new chunk if we have text remaining. + size_t newStart = length; + if (newStart < caption.text.length()) + { + caption.text = caption.text.substr(newStart); + } + else { break; } + + caption.microsecondsRemaining -= next.microsecondsRemaining; + count++; + idx = 0; + chunkLineCount = 0; + chunk = ""; + } } - if (spaceIndex > 0) - { - f32 ratio = spaceIndex / (f32)caption.text.length(); - next.text = next.text.substr(0, spaceIndex); - next.microsecondsRemaining = s64(next.microsecondsRemaining * ratio); // Fixes a float to int warning. - addCaption(next); - caption.text = caption.text.substr(spaceIndex + 1); - caption.microsecondsRemaining -= next.microsecondsRemaining; - count++; - } - else { break; } } } - else if (env == CC_Gameplay) + else if (env == CC_GAMEPLAY) { // Don't add caption if this type of caption is disabled - if (caption.type == CC_Effect && !settings->showGameplayCaptions) return; - else if (caption.type == CC_Voice && !settings->showGameplaySubtitles) return; + if (caption.type == CC_EFFECT && !settings->showGameplayCaptions) { return; } + else if (caption.type == CC_VOICE && !settings->showGameplaySubtitles) { return; } } - addCaption(caption); + enqueueCaption(caption); } } - void addCaption(const ConsoleArgList& args) + void enqueueCaption(const ConsoleArgList& args) { if (args.size() < 2) { return; } Caption caption; caption.text = args[1]; - caption.env = CC_Cutscene; + caption.env = CC_CUTSCENE; caption.microsecondsRemaining = calculateDuration(caption.text); - caption.type = CC_Voice; + caption.type = CC_VOICE; - addCaption(caption); + enqueueCaption(caption); } - void addCaption(Caption caption) + void enqueueCaption(Caption caption) { if (s_activeCaptions.size() == 0) { s_lastTime = system_clock::now().time_since_epoch(); @@ -225,6 +501,8 @@ namespace TFE_A11Y // a11y is industry slang for accessibility void drawCaptions(std::vector* captions) { + if (isFontLoaded()) { ImGui::PushFont(s_currentCaptionFont); } + TFE_Settings_A11y* settings = TFE_Settings::getA11ySettings(); // Track time elapsed since last update @@ -237,7 +515,7 @@ namespace TFE_A11Y // a11y is industry slang for accessibility s32 maxLines; // Calculate font size and window dimensions - if (captions->at(0).env == CC_Gameplay) + if (captions->at(0).env == CC_GAMEPLAY) { maxLines = settings->gameplayMaxTextLines; // If too many captions, remove oldest captions @@ -256,11 +534,11 @@ namespace TFE_A11Y // a11y is industry slang for accessibility RGBA* fontColor; //TFE_System::logWrite(LOG_ERROR, "a11y", (std::to_string(screenWidth) + " " + std::to_string(subtitleWindowSize.x) + ", " + std::to_string(screenHeight) + " " + std::to_string(subtitleWindowSize.y)).c_str()); - if (captions->at(0).env == CC_Gameplay) + if (captions->at(0).env == CC_GAMEPLAY) { - ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, settings->gameplayTextBackgroundAlpha)); //window bg + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, settings->gameplayTextBackgroundAlpha)); // Window bg f32 borderAlpha = settings->showGameplayTextBorder ? settings->gameplayTextBackgroundAlpha : 0; - ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.5f, 0.5f, 0.5f, borderAlpha)); //window border + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.5f, 0.5f, 0.5f, borderAlpha)); // Window border ImGui::SetNextWindowPos(ImVec2((f32)((s_screenWidth - windowSize.x) / 2), DEFAULT_LINE_HEIGHT * 2.0f)); ImGui::Begin("##Captions", &s_active, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoInputs); fontColor = &settings->gameplayFontColor; @@ -269,9 +547,9 @@ namespace TFE_A11Y // a11y is industry slang for accessibility } else // Cutscenes { - ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, settings->cutsceneTextBackgroundAlpha)); //window bg + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, settings->cutsceneTextBackgroundAlpha)); // Window bg f32 borderAlpha = settings->showCutsceneTextBorder ? settings->cutsceneTextBackgroundAlpha : 0; - ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.5f, 0.5f, 0.5f, borderAlpha)); //window border + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.5f, 0.5f, 0.5f, borderAlpha)); // Window border ImGui::SetNextWindowPos(ImVec2((f32)((s_screenWidth - windowSize.x) / 2), s_screenHeight - DEFAULT_LINE_HEIGHT), 0, ImVec2(0, 1)); ImGui::Begin("##Captions", &s_active, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoInputs); fontColor = &settings->cutsceneFontColor; @@ -286,10 +564,10 @@ namespace TFE_A11Y // a11y is industry slang for accessibility // Display each caption s32 totalLines = 0; - for (s32 i = 0; i < captions->size() && totalLines < maxLines; i++) + for (size_t i = 0; i < captions->size() && totalLines < maxLines; i++) { Caption* title = &captions->at(i); - bool wrapText = (title->env == CC_Cutscene); // Wrapped for cutscenes, centered for gameplay + bool wrapText = (title->env == CC_CUTSCENE); // Wrapped for cutscenes, centered for gameplay f32 wrapWidth = wrapText ? windowSize.x : -1.0f; auto textSize = ImGui::CalcTextSize(title->text.c_str(), 0, false, wrapWidth); if (!wrapText && textSize.x > windowSize.x) @@ -327,8 +605,10 @@ namespace TFE_A11Y // a11y is industry slang for accessibility } } ImGui::PopStyleColor(); - ImGui::End(); + + if (isFontLoaded()) { ImGui::PopFont(); } + s_lastTime = time; } @@ -336,11 +616,8 @@ namespace TFE_A11Y // a11y is industry slang for accessibility { if (s_exampleCaptions.size() == 0) { - Caption caption; - caption.text = "This is an example cutscene caption."; - caption.microsecondsRemaining = secondsToMicroseconds(0.5f); - caption.env = CaptionEnv::CC_Cutscene; - caption.type = CC_Voice; + Caption caption = s_captionMap["example_cutscene"]; // Copy + caption.env = CC_CUTSCENE; s_exampleCaptions.push_back(caption); } drawCaptions(&s_exampleCaptions); @@ -387,6 +664,11 @@ namespace TFE_A11Y // a11y is industry slang for accessibility } return input; } + + string toFileName(string language) + { + return FILE_NAME_START + language + FILE_NAME_EXT; + } void loadScreenSize() { @@ -402,8 +684,7 @@ namespace TFE_A11Y // a11y is industry slang for accessibility *fontScale = (f32)fmax(*fontScale, 1); f32 maxWidth = MAX_CAPTION_WIDTH; - f32 windowWidth = s_screenWidth * 0.8f; - if (env == CC_Gameplay) + if (env == CC_GAMEPLAY) { *fontScale += (f32)(settings->gameplayFontSize * s_screenHeight / 1280.0f); maxWidth += 100 * settings->gameplayFontSize; @@ -414,7 +695,9 @@ namespace TFE_A11Y // a11y is industry slang for accessibility maxWidth += 100 * settings->cutsceneFontSize; } *fontScale = (f32)fmax(*fontScale, 1); + if (s_currentCaptionFont != nullptr) *fontScale *= .7f; + f32 windowWidth = s_screenWidth * 0.8f; if (windowWidth > maxWidth) windowWidth = maxWidth; ImVec2 windowSize = ImVec2(windowWidth, 0); // Auto-size vertically return windowSize; @@ -425,7 +708,7 @@ namespace TFE_A11Y // a11y is industry slang for accessibility return (s64)(seconds * 1000000); } - s64 calculateDuration(string text) { + s64 calculateDuration(const string text) { return BASE_DURATION_MICROSECONDS + text.length() * MICROSECONDS_PER_CHAR; } } diff --git a/TheForceEngine/TFE_A11y/accessibility.h b/TheForceEngine/TFE_A11y/accessibility.h index 362fca8e6..4426633a3 100644 --- a/TheForceEngine/TFE_A11y/accessibility.h +++ b/TheForceEngine/TFE_A11y/accessibility.h @@ -1,17 +1,27 @@ #pragma once #include +#include #include +#include using std::string; namespace TFE_A11Y { + /////////////////////////////////////////// + // Enums/structs + /////////////////////////////////////////// + enum A11yStatus + { + CC_NOT_LOADED, CC_LOADED, CC_ERROR + }; + enum CaptionEnv { - CC_Gameplay, CC_Cutscene + CC_GAMEPLAY, CC_CUTSCENE }; enum CaptionType { - CC_Voice, CC_Effect + CC_VOICE, CC_EFFECT }; struct Caption @@ -22,16 +32,38 @@ namespace TFE_A11Y { CaptionEnv env; }; + /////////////////////////////////////////// + // Constants + /////////////////////////////////////////// + const string FILE_NAME_START = "subtitles-"; + const string FILE_NAME_EXT = ".txt"; + + /////////////////////////////////////////// + // Functions + /////////////////////////////////////////// void init(); - void clearCaptions(); + + // Fonts + FilePathList getFontFiles(); + FilePath getCurrentFontFile(); + void setPendingFont(const string path); + bool hasPendingFont(); + void loadPendingFont(); + + // Captions + FilePathList getCaptionFiles(); + FilePath getCurrentCaptionFile(); + A11yStatus getStatus(); + void loadCaptions(const string path); + void clearActiveCaptions(); void drawCaptions(); void drawExampleCaptions(); void focusCaptions(); - void addCaption(Caption caption); + void enqueueCaption(Caption caption); void onSoundPlay(char* name, CaptionEnv env); - //True if captions or subtitles are enabled for cutscenes + // True if captions or subtitles are enabled for cutscenes bool cutsceneCaptionsEnabled(); - //True if captions or subtitles are enabled for gameplay + // True if captions or subtitles are enabled for gameplay bool gameplayCaptionsEnabled(); } \ No newline at end of file diff --git a/TheForceEngine/TFE_A11y/filePathList.cpp b/TheForceEngine/TFE_A11y/filePathList.cpp new file mode 100644 index 000000000..d4bd7da20 --- /dev/null +++ b/TheForceEngine/TFE_A11y/filePathList.cpp @@ -0,0 +1,56 @@ +#include "filePathList.h" +#include + +namespace TFE_A11Y { + + FilePathList::FilePathList() + { + } + + void FilePathList::addFiles(const char directoryPath[], const char extension[], std::function filterFunc) + { + FileList dirList; + FileUtil::readDirectory(directoryPath, extension, dirList); + if (!dirList.empty()) + { + const size_t count = dirList.size(); + const std::string* dir = dirList.data(); + for (size_t d = 0; d < count; d++) + { + if (filterFunc == nullptr || filterFunc(dir[d])) + { + m_fileNames.push_back(dir[d]); + m_filePaths.push_back(directoryPath + dir[d]); + } + } + } + } + + /**/ + FilePath FilePathList::at(size_t index) + { + if (index < 0 || index >= count()) { return FilePath(nullptr, nullptr); } + return FilePath(m_fileNames.at(index), m_filePaths.at(index)); + } + + void FilePathList::clear() + { + m_fileNames.clear(); + m_filePaths.clear(); + } + + size_t FilePathList::count() + { + return m_filePaths.size(); + } + + std::vector* FilePathList::getFilePaths() + { + return &m_filePaths; + } + + std::vector* FilePathList::getFileNames() + { + return &m_fileNames; + } +} \ No newline at end of file diff --git a/TheForceEngine/TFE_A11y/filePathList.h b/TheForceEngine/TFE_A11y/filePathList.h new file mode 100644 index 000000000..cc6010063 --- /dev/null +++ b/TheForceEngine/TFE_A11y/filePathList.h @@ -0,0 +1,42 @@ +#pragma once +#include +#include +#include +using std::string; + +namespace TFE_A11Y { + + // Pairs together a file name and full path to the file. + struct FilePath { + string name; + string path; + + FilePath() = default; + + FilePath(const string& name, const string& path) + { + this->name = name; + this->path = path; + } + }; + + // Stores parallel lists of file names and paths. Useful when displaying a dropdown + // list of files in the front-end UI. + class FilePathList + { + public: + FilePathList(); + + void addFiles(const char directoryPath[], const char extension[], std::function filterFunc = nullptr); + FilePath at(size_t index); + void clear(); + + size_t count(); + std::vector* getFilePaths(); + std::vector* getFileNames(); + + private: + std::vector m_filePaths; + std::vector m_fileNames; + }; +} \ No newline at end of file diff --git a/TheForceEngine/TFE_DarkForces/Landru/cutscene_player.cpp b/TheForceEngine/TFE_DarkForces/Landru/cutscene_player.cpp index 56eb17249..798c05248 100644 --- a/TheForceEngine/TFE_DarkForces/Landru/cutscene_player.cpp +++ b/TheForceEngine/TFE_DarkForces/Landru/cutscene_player.cpp @@ -218,7 +218,7 @@ namespace TFE_DarkForces else if (TFE_Input::keyPressed(KEY_SPACE)) { s_nextSceneInput = JTRUE; - if (TFE_A11Y::cutsceneCaptionsEnabled()) TFE_A11Y::clearCaptions(); + if (TFE_A11Y::cutsceneCaptionsEnabled()) TFE_A11Y::clearActiveCaptions(); } ///////////////////////////////// diff --git a/TheForceEngine/TFE_DarkForces/Landru/lsound.cpp b/TheForceEngine/TFE_DarkForces/Landru/lsound.cpp index 6a2c760fc..f0b7e07d6 100644 --- a/TheForceEngine/TFE_DarkForces/Landru/lsound.cpp +++ b/TheForceEngine/TFE_DarkForces/Landru/lsound.cpp @@ -133,13 +133,13 @@ namespace TFE_DarkForces ///////////////////////////////////////////////////////// void startSfx(LSound* sound) { - TFE_A11Y::onSoundPlay(sound->name, TFE_A11Y::CaptionEnv::CC_Cutscene); + TFE_A11Y::onSoundPlay(sound->name, TFE_A11Y::CaptionEnv::CC_CUTSCENE); ImStartSfx((ImSoundId)sound, DEFAULT_PRIORITY); } void startSpeech(LSound* sound) { - TFE_A11Y::onSoundPlay(sound->name, TFE_A11Y::CaptionEnv::CC_Cutscene); + TFE_A11Y::onSoundPlay(sound->name, TFE_A11Y::CaptionEnv::CC_CUTSCENE); ImStartVoice((ImSoundId)sound, DEFAULT_PRIORITY); } diff --git a/TheForceEngine/TFE_DarkForces/darkForcesMain.cpp b/TheForceEngine/TFE_DarkForces/darkForcesMain.cpp index 5508f752b..1f27c292e 100644 --- a/TheForceEngine/TFE_DarkForces/darkForcesMain.cpp +++ b/TheForceEngine/TFE_DarkForces/darkForcesMain.cpp @@ -599,7 +599,7 @@ namespace TFE_DarkForces { startNextMode(); } - TFE_A11Y::clearCaptions(); + TFE_A11Y::clearActiveCaptions(); } } break; case GSTATE_BRIEFING: @@ -668,7 +668,7 @@ namespace TFE_DarkForces bitmap_clearLevelData(); bitmap_setAllocator(s_gameRegion); level_freeAllAssets(); - TFE_A11Y::clearCaptions(); + TFE_A11Y::clearActiveCaptions(); } } break; } diff --git a/TheForceEngine/TFE_DarkForces/sound.cpp b/TheForceEngine/TFE_DarkForces/sound.cpp index 1f8ad07d2..5bfc860af 100644 --- a/TheForceEngine/TFE_DarkForces/sound.cpp +++ b/TheForceEngine/TFE_DarkForces/sound.cpp @@ -294,7 +294,7 @@ namespace TFE_DarkForces sound_setVolume(idInstance, sound->volume); if (withCaptions && TFE_A11Y::gameplayCaptionsEnabled()) { - TFE_A11Y::onSoundPlay(sound->name, TFE_A11Y::CaptionEnv::CC_Gameplay); + TFE_A11Y::onSoundPlay(sound->name, TFE_A11Y::CaptionEnv::CC_GAMEPLAY); } return idInstance; @@ -425,7 +425,7 @@ namespace TFE_DarkForces // TFE subtitles/captions if (TFE_A11Y::gameplayCaptionsEnabled() && *vol > TFE_Settings::getA11ySettings()->gameplayCaptionMinVolume) { - TFE_A11Y::onSoundPlay(sound->name, TFE_A11Y::CaptionEnv::CC_Gameplay); + TFE_A11Y::onSoundPlay(sound->name, TFE_A11Y::CaptionEnv::CC_GAMEPLAY); } } diff --git a/TheForceEngine/TFE_FrontEndUI/frontEndUi.cpp b/TheForceEngine/TFE_FrontEndUI/frontEndUi.cpp index 32d1a0cbf..0593ba591 100644 --- a/TheForceEngine/TFE_FrontEndUI/frontEndUi.cpp +++ b/TheForceEngine/TFE_FrontEndUI/frontEndUi.cpp @@ -2626,7 +2626,7 @@ namespace TFE_FrontEndUI ImGui::LabelText("##ConfigLabel", "%s", label); ImGui::PopStyleColor(); ImGui::SameLine(); - RGBAf c; + RGBAf c = RGBAf(); c.r = color->getRedF(); c.g = color->getGreenF(); c.b = color->getBlueF(); @@ -2656,17 +2656,68 @@ namespace TFE_FrontEndUI ImGui::SetNextItemWidth(valueWidth); ImGui::SliderInt(tag, value, min, max); } + + string DrawFileListCombo(const char* tag, string currentFileName, string currentFilePath, TFE_A11Y::FilePathList filePathList) + { + // We only display the file name in the dropdown, but internally track the full path. + if (ImGui::BeginCombo(tag, currentFileName.c_str())) + { + std::vector* names = filePathList.getFileNames(); + std::vector* paths = filePathList.getFilePaths(); + + for (int n = 0; n < names->size(); n++) + { + string name = names->at(n); + string path = paths->at(n); + bool is_selected = (currentFilePath == path); + if (ImGui::Selectable(name.c_str(), is_selected)) { currentFilePath = path; } + if (is_selected) + { + ImGui::SetItemDefaultFocus(); + } + } + ImGui::EndCombo(); + } + return currentFilePath; + } - //Accessibility + // Accessibility void configA11y() { TFE_Settings_A11y* a11y = TFE_Settings::getA11ySettings(); float labelW = 140 * s_uiScale; float valueW = 260 * s_uiScale; - //CUTSCENES ----------------------------------------- + if (TFE_A11Y::getStatus() == TFE_A11Y::CC_ERROR) + { + ImGui::LabelText("##ConfigLabel", "Error: Caption file could not be loaded!"); + } + + // Caption file dropdown. + ImGui::LabelText("##ConfigLabel", "Subtitle/caption file:"); + TFE_A11Y::FilePath currentCaptionFile = TFE_A11Y::getCurrentCaptionFile(); + string currentCaptionFilePath = DrawFileListCombo("##ccfile", currentCaptionFile.name, currentCaptionFile.path, TFE_A11Y::getCaptionFiles()); + // If user changed the selected caption file, reload captions + if (currentCaptionFilePath != currentCaptionFile.path) + { + TFE_A11Y::clearActiveCaptions(); + TFE_A11Y::loadCaptions(currentCaptionFilePath); + } + + // Font file dropdown. + ImGui::LabelText("##ConfigLabel", "Font:"); + TFE_A11Y::FilePath currentFont = TFE_A11Y::getCurrentFontFile(); + string currentFontPath = DrawFileListCombo("##fontfile", currentFont.name, currentFont.path, TFE_A11Y::getFontFiles()); + // If user changed the selected font file, queue the font to load after we finish rendering ImGui + // (we can't add new fonts to the ImGui font atlas while ImGui is active). + if (currentFontPath != currentFont.path) + { + TFE_A11Y::setPendingFont(currentFontPath); + } + + // CUTSCENES ----------------------------------------- ImGui::PushFont(s_dialogFont); - ImGui::LabelText("##ConfigLabel2", "Cutscenes"); + ImGui::LabelText("##ConfigLabel", "Cutscenes"); ImGui::PopFont(); ImGui::Checkbox("Subtitles (voice)##Cutscenes", &a11y->showCutsceneSubtitles); diff --git a/TheForceEngine/TFE_Settings/settings.cpp b/TheForceEngine/TFE_Settings/settings.cpp index 71a8679e3..161660acb 100644 --- a/TheForceEngine/TFE_Settings/settings.cpp +++ b/TheForceEngine/TFE_Settings/settings.cpp @@ -445,6 +445,9 @@ namespace TFE_Settings void writeA11ySettings(FileStream& settings) { writeHeader(settings, c_sectionNames[SECTION_A11Y]); + writeKeyValue_String(settings, "language", s_a11ySettings.language.c_str()); + writeKeyValue_String(settings, "lastFontPath", s_a11ySettings.lastFontPath.c_str()); + writeKeyValue_Bool(settings, "showCutsceneSubtitles", s_a11ySettings.showCutsceneSubtitles); writeKeyValue_Bool(settings, "showCutsceneCaptions", s_a11ySettings.showCutsceneCaptions); writeKeyValue_Int(settings, "cutsceneFontSize", s_a11ySettings.cutsceneFontSize); @@ -916,7 +919,15 @@ namespace TFE_Settings void parseA11ySettings(const char* key, const char* value) { - if (strcasecmp("showCutsceneSubtitles", key) == 0) + if (strcasecmp("language", key) == 0) + { + s_a11ySettings.language = value; + } + else if (strcasecmp("lastFontPath", key) == 0) + { + s_a11ySettings.lastFontPath = value; + } + else if (strcasecmp("showCutsceneSubtitles", key) == 0) { s_a11ySettings.showCutsceneSubtitles = parseBool(value); } diff --git a/TheForceEngine/TFE_Settings/settings.h b/TheForceEngine/TFE_Settings/settings.h index 130cdded8..a5571ba9f 100644 --- a/TheForceEngine/TFE_Settings/settings.h +++ b/TheForceEngine/TFE_Settings/settings.h @@ -257,23 +257,26 @@ struct RGBAf { struct TFE_Settings_A11y { - bool showCutsceneSubtitles; //voice - bool showCutsceneCaptions; //descriptive (e.g. "[Mine beeping]", "[Engine roaring]" + string language = "en"; //ISO 639-1 two-letter code + string lastFontPath; + + bool showCutsceneSubtitles; // Voice + bool showCutsceneCaptions; // Descriptive (e.g. "[Mine beeping]", "[Engine roaring]" FontSize cutsceneFontSize; RGBA cutsceneFontColor = RGBA::fromFloats(1.0f, 1.0f, 1.0f); f32 cutsceneTextBackgroundAlpha = 0.75f; bool showCutsceneTextBorder = true; f32 cutsceneTextSpeed = 1.0f; - bool showGameplaySubtitles; //voice - bool showGameplayCaptions; //descriptive + bool showGameplaySubtitles; // Voice + bool showGameplayCaptions; // Descriptive FontSize gameplayFontSize; RGBA gameplayFontColor = RGBA::fromFloats(1.0f, 1.0f, 1.0f); int gameplayMaxTextLines = 3; f32 gameplayTextBackgroundAlpha = 0.0f; bool showGameplayTextBorder = false; f32 gameplayTextSpeed = 1.0f; - s32 gameplayCaptionMinVolume = 32; //in range 0 - 127 + s32 gameplayCaptionMinVolume = 32; // In range 0 - 127 }; namespace TFE_Settings diff --git a/TheForceEngine/TheForceEngine.vcxproj b/TheForceEngine/TheForceEngine.vcxproj index 84368e162..807d04713 100644 --- a/TheForceEngine/TheForceEngine.vcxproj +++ b/TheForceEngine/TheForceEngine.vcxproj @@ -330,6 +330,7 @@ echo ^)"; + @@ -693,6 +694,7 @@ echo ^)"; + diff --git a/TheForceEngine/TheForceEngine.vcxproj.filters b/TheForceEngine/TheForceEngine.vcxproj.filters index 49259b160..a3d19290c 100644 --- a/TheForceEngine/TheForceEngine.vcxproj.filters +++ b/TheForceEngine/TheForceEngine.vcxproj.filters @@ -1276,6 +1276,9 @@ Source\TFE_Jedi\Level + + Source\TFE_A11y + @@ -2154,6 +2157,9 @@ Source\TFE_Jedi\Level + + Source\TFE_A11y + diff --git a/TheForceEngine/main.cpp b/TheForceEngine/main.cpp index caba7f0ac..7744bf1a5 100644 --- a/TheForceEngine/main.cpp +++ b/TheForceEngine/main.cpp @@ -754,6 +754,7 @@ int main(int argc, char* argv[]) } } + if (TFE_A11Y::hasPendingFont()) { TFE_A11Y::loadPendingFont(); } // Can't load new fonts between TFE_Ui::begin() and TFE_Ui::render(); TFE_Ui::begin(); TFE_System::update();