├── arbm.bmp ├── ra3bar.bmp ├── ra3bar.array ├── resource.res ├── ra3interesting.ico ├── ra3barlauncher_chinese.csf ├── ra3barlauncher_english.csf ├── resource.h ├── RA3.exe.Manifest ├── resource.rc ├── LICENSE ├── .github └── workflows │ └── main.yml ├── UserInterface.hpp ├── README.md ├── Input.hpp ├── common.hpp ├── CSFParser.hpp ├── main.cpp ├── ReplaysAndMods.hpp ├── WindowsWrapper.hpp └── UserInterface.cpp /arbm.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lanyizi/RA3Bar-RA3Launcher/HEAD/arbm.bmp -------------------------------------------------------------------------------- /ra3bar.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lanyizi/RA3Bar-RA3Launcher/HEAD/ra3bar.bmp -------------------------------------------------------------------------------- /ra3bar.array: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lanyizi/RA3Bar-RA3Launcher/HEAD/ra3bar.array -------------------------------------------------------------------------------- /resource.res: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lanyizi/RA3Bar-RA3Launcher/HEAD/resource.res -------------------------------------------------------------------------------- /ra3interesting.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lanyizi/RA3Bar-RA3Launcher/HEAD/ra3interesting.ico -------------------------------------------------------------------------------- /ra3barlauncher_chinese.csf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lanyizi/RA3Bar-RA3Launcher/HEAD/ra3barlauncher_chinese.csf -------------------------------------------------------------------------------- /ra3barlauncher_english.csf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lanyizi/RA3Bar-RA3Launcher/HEAD/ra3barlauncher_english.csf -------------------------------------------------------------------------------- /resource.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | #ifndef RT_MANIFEST 5 | #define RT_MANIFEST 24 6 | #endif 7 | 8 | #define DEFAULT_ICON 200 9 | #define AR_BACKGROUND 201 10 | #define RA3BAR_LOGO 202 11 | 12 | #define BUILTIN_ENGLISH 300 13 | #define BUILTIN_CHINESE 301 14 | -------------------------------------------------------------------------------- /RA3.exe.Manifest: -------------------------------------------------------------------------------- 1 | 2 | 5 | 10 | [RA3Bar] Red Alert 3 Game Launcher 11 | 12 | 13 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /resource.rc: -------------------------------------------------------------------------------- 1 | #include "resource.h" 2 | 3 | 1 RT_MANIFEST "RA3.exe.Manifest" 4 | 1 VERSIONINFO 5 | FILEVERSION 0,9,2,0 6 | PRODUCTVERSION 0,9,2,0 7 | FILETYPE VFT_APP 8 | { 9 | BLOCK "StringFileInfo" 10 | { 11 | BLOCK "040904E4" 12 | { 13 | VALUE "CompanyName", "" 14 | VALUE "FileVersion", "0.9.2.0" 15 | VALUE "FileDescription", "Unofficial player-made Red Alert 3 game launcher" 16 | VALUE "InternalName", "" 17 | VALUE "OriginalFilename", "" 18 | VALUE "ProductName", "[RA3Bar] Red Alert 3 QuickLoader" 19 | VALUE "ProductVersion", "0.9.2.0" 20 | } 21 | } 22 | BLOCK "VarFileInfo" { 23 | VALUE "Translation", 0x0409, 1252 24 | } 25 | } 26 | 27 | DEFAULT_ICON ICON "ra3interesting.ico" 28 | AR_BACKGROUND BITMAP "arbm.bmp" 29 | RA3BAR_LOGO RCDATA "ra3bar.array" 30 | BUILTIN_ENGLISH RCDATA "ra3barlauncher_english.csf" 31 | BUILTIN_CHINESE RCDATA "ra3barlauncher_chinese.csf" 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Lanyi 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. 22 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build Binary 2 | 3 | # Controls when the workflow will run 4 | on: 5 | # Triggers the workflow on push or pull request events but only for the master branch 6 | push: 7 | branches: [ master ] 8 | pull_request: 9 | branches: [ master ] 10 | 11 | # Allows you to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | jobs: 15 | build: 16 | runs-on: windows-latest 17 | steps: 18 | - name: Install MinGW 19 | uses: egor-tensin/setup-mingw@v2 20 | with: 21 | platform: x86 22 | 23 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 24 | - uses: actions/checkout@v2 25 | 26 | - name: Build RA3.exe 27 | run: |- 28 | g++ -c UserInterface.cpp -o UserInterface.o -Os -Wall -std=c++17 -DUNICODE -D_UNICODE 29 | g++ -c main.cpp -o main.o -Wall -std=c++17 -Os -DUNICODE -D_UNICODE 30 | windres -i resource.rc -o resource.res -O coff 31 | g++ UserInterface.o main.o resource.res -o RA3.exe -mwindows -static-libgcc -static-libstdc++ -lComctl32 -lShlwapi -static -lpthread -s 32 | - name: Upload a Build Artifact 33 | uses: actions/upload-artifact@v2.2.4 34 | with: 35 | # Artifact name 36 | name: RA3.exe 37 | # A file, directory or wildcard pattern that describes what to upload 38 | path: RA3.exe 39 | retention-days: 90 40 | 41 | -------------------------------------------------------------------------------- /UserInterface.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "Common.hpp" 7 | 8 | using LanguageList = std::vector; 9 | 10 | enum class SplashScreenResult : INT_PTR { 11 | normal, 12 | clicked, 13 | }; 14 | 15 | struct LaunchOptions { 16 | enum LoadFileType { 17 | noFile, 18 | replay, 19 | mod, 20 | }; 21 | LoadFileType loadFileType; 22 | std::wstring fileToBeLoaded; 23 | std::wstring extraCommandLine; 24 | }; 25 | 26 | void displayErrorMessage(const std::exception& error, const LanguageData& languageData); 27 | void notifyCantUpdate(const LanguageData& languageData); 28 | void notifyIsNotReplay(const LanguageData& languageData); 29 | bool askUserIfFixReplay(const std::wstring& replayName, const LanguageData& languageData); 30 | void notifyFixReplaySucceeded(const LanguageData& languageData); 31 | void notifyFixReplayFailed(const std::exception& errorMessage, const LanguageData& languageData); 32 | void notifyNoCommentatorAvailable(const LanguageData& languageData); 33 | void notifyGameVersionNotFound(const std::wstring& version, const LanguageData& languageData); 34 | void notifyReplayModNotFound(const LanguageData& languageData); 35 | void notifyReplayModAmbiguity(const LanguageData& languageData); 36 | void notifyModNotFound(const LanguageData& languageData); 37 | 38 | SplashScreenResult displaySplashScreen(const std::wstring& ra3Path, const LanguageData& languageData); 39 | std::optional runControlCenter(const std::wstring& ra3Path, const std::wstring& userCommandLine, LanguageData& languageData, HBITMAP customBackground); 40 | 41 | 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RA3Bar-RA3QuickLoader 2 | For more details about this project: https://www.gamereplays.org/community/index.php?showtopic=1013524 3 | 4 | Or also here if you can speak Chinese: https://tieba.baidu.com/p/5735228567 5 | 6 | ## Features 7 | * Start Red Alert 3 nearly instantly 8 | * You can now click Red Alert 3's splash screen to open RA3's Control Center. This could be pretty handy if you wish to play Mods[1] with current version of C&C:Online Launcher. 9 | * You can now add custom command-line arguments directly inside the Control Center. Again, this could be pretty handy if you want to add some command line arguments when using C&C:Online Launcher. 10 | * Control Center's Game Browser is now equipped with a simple replay parser which can display some match information such as date, game duration, map, and player list. It can also fix corrupted replays damaged by game crash. 11 | * If you can't open (and watch) .RA3Replay files with RA3.exe, now you can set the replay file association in a rather easy way: right click on the replay file, and choose (this new) RA3.exe in the **Open With...** menu. Then you can watch replays by just double-clicking on the file. 12 | 13 | The replay parsing functions of this project is largerly based on [louisdx's cnc3reader](https://github.com/louisdx/cnc-replayreaders/). 14 | 15 | [1]: In case you don't know how to load Mods: you need go to Control Center → Game Browser → Mod. To install a Mod, put them in `My Documents\Red Alert 3\Mods`. 16 | 17 | ## How to build 18 | If you are using MinGW64, you can build like this: 19 | 20 | ``` 21 | g++ -c UserInterface.cpp -o UserInterface.o -Wall -std=c++17 -DUNICODE -D_UNICODE 22 | g++ -c main.cpp -o main.o -Wall -std=c++17 -DUNICODE -D_UNICODE 23 | windres -i resource.rc -o resource.res -O coff 24 | g++ UserInterface.o main.o resource.res -o RA3.exe -mwindows -static-libgcc -static-libstdc++ -lComctl32 -lShlwapi -static -lpthread 25 | ``` 26 | 27 | 28 | I think Visual Studio should also be able to build with these files without any problems, but I haven't tried it yet. 29 | 30 | ## About this program 31 | Recently a lot of people needs to wait for like 30 seconds when launching Red Alert 3. 32 | 33 | This is because Red Alert 3's game launcher, **RA3.exe**, will firstly check "Comrade News"[2] before starting the main game process. But EA recently shut down the server files.ea.com, and RA3.exe would need to wait until the connection times out, before starting the game. 34 | 35 | While this problem can be fixed in some rather simple way, such as by editing the `host` file or by cutting off the Internet connection when launching the game (so the connection to EA server would fail instantly instead of waiting 30 seconds), I thought I could write a my own RA3.exe to solve this problem. After all, RA3.exe is just a game launcher, isn't it? 36 | It looks like I just need write a program which calls `CreateProcess()` to start game's main process, and this should be a really simple task! 37 | 38 | So I started this project by using only Win32 API (because I thought it would be enough), and eventually I discovered that... the things aren't that easy like what I thought... 39 | And I'm not very good at coding (you can confirm this by reading my code :P), so this project has nearly become a big pain for me, but fortunately, I still completed it! 40 | 41 | 42 | [2]: Example link of Comrade News: http://files.ea.com/downloads/eagames/cc/tiberium/RedAlert3/RA3ComradeNews/RA3ComradeNews_english.html 43 | -------------------------------------------------------------------------------- /Input.hpp: -------------------------------------------------------------------------------- 1 | //Some utility functions used to 2 | //read binary data from a pair of iterators. 3 | #pragma once 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | namespace Input { 14 | template 15 | struct Range { 16 | struct RangeForSupport { 17 | constexpr Iterator begin() const noexcept { return rangeBegin; } 18 | constexpr Iterator end() const noexcept { return rangeEnd; } 19 | Iterator rangeBegin; 20 | Iterator rangeEnd; 21 | }; 22 | using Category = typename std::iterator_traits::iterator_category; 23 | enum { isAtLeastForwardIterator = std::is_base_of_v }; 24 | constexpr Range(const Iterator& current, const Iterator& end) noexcept : current{current}, end{end} { } 25 | constexpr RangeForSupport rangeForLoop() const noexcept { 26 | static_assert(isAtLeastForwardIterator, "Using Range with a range for loop would leave dangling iterators."); 27 | return RangeForSupport{this->current, this->end}; 28 | } 29 | Iterator current; 30 | Iterator end; 31 | }; 32 | 33 | struct RangeException : std::out_of_range { 34 | using std::out_of_range::out_of_range; 35 | }; 36 | 37 | //return the [begin + magicSize, end] on success 38 | template 39 | void readAndCheckMagic(Range& range, std::string_view magic) { 40 | auto [magicEnd, current] = std::mismatch(magic.begin(), magic.end(), range.current, range.end); 41 | if(magicEnd != magic.end()) { 42 | if(current == range.end) { 43 | throw RangeException("Iterator reached end before compare finishes."); 44 | } 45 | constexpr auto toHex = [](unsigned char byte) -> char { byte = byte bitand 0xF; return byte < 0xA ? byte + '0' : byte - 0xA + 'A'; }; 46 | auto printable = std::string{}; 47 | for(auto byte : magic) { printable += isprint(byte) ? std::string{byte} : std::string{'\\', 'x', toHex(byte >> 4), toHex(byte)}; } 48 | throw std::invalid_argument("May not be able to parse file correctly, \"" + printable + "\" not found."); 49 | } 50 | range.current = current; 51 | } 52 | 53 | template 54 | void copyFixed(Range& range, OutputIterator outputBegin, typename std::iterator_traits::difference_type count) { 55 | if constexpr(range.isAtLeastForwardIterator) { 56 | if(std::distance(range.current, range.end) < count) { 57 | throw RangeException("ForwardIterator will reach end before copy finishes"); 58 | } 59 | auto afterCopy = std::next(range.current, count); 60 | std::copy(range.current, afterCopy, outputBegin); 61 | range.current = afterCopy; 62 | } 63 | else { 64 | for(auto k = count; k != 0; --k) { 65 | if(range.current == range.end) { 66 | throw RangeException("InputIterator reached end before copy finishes"); 67 | } 68 | *outputBegin = *range.current; 69 | ++range.current, ++outputBegin; 70 | } 71 | } 72 | } 73 | 74 | template 75 | void ignore(Range& range, std::size_t numberOfBytes) { 76 | if constexpr(range.isAtLeastForwardIterator) { 77 | auto distance = std::distance(range.current, range.end); 78 | if((distance < 0) or (static_cast(distance) < numberOfBytes)) { 79 | throw RangeException("ForwardIterator will reach end before ignore finishes"); 80 | } 81 | range.current = std::next(range.current, numberOfBytes); 82 | } 83 | else { 84 | for(auto k = numberOfBytes; k != 0; --k) { 85 | if(range.current == range.end) { 86 | throw RangeException("InputIterator reached end before ignore finishes"); 87 | } 88 | ++range.current; 89 | } 90 | } 91 | } 92 | 93 | template 94 | void ignore(Range& range) { 95 | return ignore(range, sizeof(T)); 96 | } 97 | 98 | template 99 | T copyBytes(Range& input) { 100 | static_assert(sizeof(char) == sizeof(std::uint8_t) and sizeof(char) == sizeof(std::byte)); 101 | using Type = std::decay_t::value_type>; 102 | using std::is_same_v; 103 | static_assert((is_same_v or is_same_v or is_same_v) and (sizeof(Type) == 1), 104 | "cannot use iterator's value_type because of strict aliasing rule!"); 105 | auto value = T{}; 106 | auto valueBegin = reinterpret_cast(&value); 107 | copyFixed(input, valueBegin, sizeof(value)); 108 | return value; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /common.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include "WindowsWrapper.hpp" 13 | #include "CSFParser.hpp" 14 | #include "ReplaysAndMods.hpp" 15 | 16 | template 17 | struct CaseInsensitiveCompare { 18 | bool operator()(wchar_t a, wchar_t b) const noexcept { 19 | return Predicate{}(std::tolower(a, std::locale::classic()), std::tolower(b, std::locale::classic())); 20 | }; 21 | }; 22 | 23 | template 24 | bool insensitiveEqual(InputIterator1 begin1, InputIterator1 end1, 25 | InputIterator2 begin2, InputIterator2 end2) { 26 | return std::equal(begin1, end1, begin2, end2, CaseInsensitiveCompare> {}); 27 | } 28 | 29 | template 30 | InputIterator1 insensitiveSearch(InputIterator1 begin1, InputIterator1 end1, 31 | InputIterator2 begin2, InputIterator2 end2) { 32 | return std::search(begin1, end1, begin2, end2, CaseInsensitiveCompare> {}); 33 | } 34 | 35 | struct StringLess { 36 | template 37 | bool operator()(const A& a, const B& b) const noexcept { 38 | return std::lexicographical_compare(std::begin(a), std::end(a), std::begin(b), std::end(b), 39 | CaseInsensitiveCompare> {}); 40 | } 41 | }; 42 | 43 | template 44 | void readFile(HANDLE file, std::vector& buffer, std::size_t count) { 45 | buffer.resize(buffer.size() + count); 46 | auto bufferStart = buffer.data() + buffer.size() - count; 47 | auto totalBytesRead = typename std::vector::size_type{0}; 48 | while(totalBytesRead < count) { 49 | using namespace Windows; 50 | auto bytesRead = DWORD{}; 51 | ReadFile(file, bufferStart + totalBytesRead, count - totalBytesRead, &bytesRead, nullptr) 52 | >> checkWin32Result("ReadFile", errorValue, false); 53 | totalBytesRead += bytesRead; 54 | 55 | if(bytesRead == 0) { 56 | throw std::runtime_error("Read 0 bytes from file"); 57 | } 58 | } 59 | } 60 | 61 | template 62 | std::vector readFile(HANDLE file, std::size_t count) { 63 | auto buffer = std::vector {}; 64 | readFile(file, buffer, count); 65 | return buffer; 66 | } 67 | 68 | template 69 | std::vector readEntireFile(HANDLE file) { 70 | auto buffer = std::vector {}; 71 | readFile(file, buffer, Windows::getFileSize(file)); 72 | return buffer; 73 | } 74 | 75 | template 76 | void writeEntireFile(HANDLE file, const std::vector& buffer) { 77 | auto totalBytesWritten = typename std::vector::size_type{0}; 78 | while(totalBytesWritten < buffer.size()) { 79 | using namespace Windows; 80 | auto bytesWritten = DWORD{}; 81 | WriteFile(file, buffer.data() + totalBytesWritten, buffer.size() - totalBytesWritten, &bytesWritten, nullptr) 82 | >> checkWin32Result("WriteFile", errorValue, false); 83 | totalBytesWritten += bytesWritten; 84 | 85 | if(bytesWritten == 0) { 86 | throw std::runtime_error("Wrote 0 bytes to file"); 87 | } 88 | } 89 | } 90 | 91 | inline void fixReplayByFileName(const std::wstring& fileName) { 92 | using namespace Windows; 93 | using namespace ReplaysAndMods; 94 | auto fixedFileContent = std::vector {}; 95 | 96 | { 97 | auto file = createFile(fileName, GENERIC_READ, FILE_SHARE_READ|FILE_SHARE_WRITE, OPEN_EXISTING); 98 | auto fileSize = getFileSize(file.get()); 99 | const auto fileContent = readFile(file.get(), fileSize); 100 | fixedFileContent = fixReplay(Input::Range{std::begin(fileContent), std::end(fileContent)}); 101 | } 102 | 103 | auto outputName = fileName + L".RA3BARLAUNCHER_FIX_REPLAY" + replayExtension; 104 | 105 | auto backupFileName = fileName + L".original"; 106 | while(fileExists(backupFileName + replayExtension)) { 107 | backupFileName += L'l'; 108 | } 109 | backupFileName += replayExtension; 110 | 111 | { 112 | auto output = createFile(outputName, GENERIC_WRITE, FILE_SHARE_READ|FILE_SHARE_WRITE, CREATE_NEW); 113 | writeEntireFile(output.get(), fixedFileContent); 114 | } 115 | 116 | ReplaceFileW(fileName.c_str(), outputName.c_str(), backupFileName.c_str(), REPLACEFILE_IGNORE_MERGE_ERRORS, nullptr, nullptr) 117 | >> checkWin32Result("ReplaceFileW", errorValue, 0); 118 | } 119 | 120 | inline Windows::RegistryKey getRa3RegistryKey(HKEY base, REGSAM access = KEY_READ) { 121 | return Windows::openRegistryKey(base, L"Software\\Electronic Arts\\Electronic Arts\\Red Alert 3", access | KEY_READ); 122 | } 123 | 124 | struct LanguageData { 125 | using ContainerType = std::map; 126 | std::wstring languageName; 127 | ContainerType table; 128 | }; 129 | 130 | inline std::vector getAllLanguages(const std::wstring& ra3Path) { 131 | using namespace Windows; 132 | auto csfs = findAllMatchingFiles(concatenatePath(ra3Path, L"Launcher\\*.csf")); 133 | for(auto& fileName : csfs) { 134 | constexpr auto csfExtentionSize = std::wstring_view{L".csf"}.size(); 135 | fileName.erase(fileName.size() - csfExtentionSize); 136 | } 137 | return csfs; 138 | } 139 | 140 | inline std::vector getSkudefs(const std::wstring& ra3Path, const std::wstring& languageName) { 141 | using namespace Windows; 142 | return findAllMatchingFiles(concatenatePath(ra3Path, L"ra3_" + languageName + L"_*.skudef")); 143 | } 144 | 145 | inline void setLanguageToRegistry(const std::wstring& language) { 146 | try { 147 | Windows::setRegistryString(getRa3RegistryKey(HKEY_CURRENT_USER, KEY_WRITE).get(), L"Language", language); 148 | } 149 | catch(...) { } 150 | } 151 | 152 | template 153 | LanguageData::ContainerType loadCSFStrings(InputIterator begin, InputIterator end) { 154 | auto emplacer = [](LanguageData::ContainerType& container, std::pair&& newValue) { 155 | for(auto& character : newValue.first) { 156 | character = std::tolower(character, std::locale::classic()); 157 | } 158 | container.emplace_hint(std::end(container), Windows::toWide(newValue.first), std::move(newValue.second)); 159 | }; 160 | return MyCSF::readCSF(Input::Range{begin, end}, emplacer); 161 | } 162 | 163 | inline LanguageData loadLanguageData(const std::wstring& ra3Path, const std::wstring& languageName) { 164 | using namespace Windows; 165 | setLanguageToRegistry(languageName); 166 | auto fileName = concatenatePath(ra3Path, L"Launcher\\" + languageName + L".csf"); 167 | auto csfContent = readEntireFile(createFile(fileName, GENERIC_READ, FILE_SHARE_READ, OPEN_EXISTING).get()); 168 | auto strings = loadCSFStrings(std::begin(csfContent), std::end(csfContent)); 169 | return {languageName, strings}; 170 | } 171 | 172 | inline LanguageData loadPreferredLanguageData(const std::wstring& ra3Path) { 173 | auto language = std::wstring{L"english"}; 174 | try { 175 | language = Windows::getRegistryString(getRa3RegistryKey(HKEY_CURRENT_USER).get(), L"Language"); 176 | } 177 | catch(...) { } //If we are unable to retrieve user language from registry, then set preferred language is English 178 | auto allLanguages = getAllLanguages(ra3Path); 179 | if(allLanguages.empty()) { 180 | throw std::runtime_error("Please install at least one language pack."); 181 | } 182 | if(auto position = std::find(std::begin(allLanguages), std::end(allLanguages), language); 183 | position != std::end(allLanguages)) { 184 | return loadLanguageData(ra3Path, *position); //if preferredn language pack exits, then return it 185 | } 186 | return loadLanguageData(ra3Path, allLanguages.front()); //otherwide return what is available 187 | } 188 | 189 | -------------------------------------------------------------------------------- /CSFParser.hpp: -------------------------------------------------------------------------------- 1 | //Simple CSF Parser 2 | //In Red Alert 3, *.csf files are used to store strings. 3 | //This header provides a function capable of parsing this kind of file. 4 | //Some other C&C Games uses CSF files too, but I didn't investigate them, 5 | //so I can't be sure that their formats are the same as RA3's one. 6 | //This file requires another header 'Input.hpp', 7 | //which provides some utility functions used to 8 | //read binary data from a pair of iterators. 9 | // - Lanyi, 2018 10 | 11 | #pragma once 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include "Input.hpp" 19 | 20 | namespace MyCSF { 21 | 22 | //Read CSF into a container using a user-provided CSFSringEmplacer. 23 | //CSFSringEmplacer accepts a reference to the destination container and 24 | //a std::pair, which is the CSF label-string pair. 25 | //Example: 26 | // using namespace std; 27 | // using namespace placeholders; 28 | // using ValueType = pair; 29 | // auto emplacer = bind(&vector::emplace_back, _1, _2); 30 | // auto file = ifstream{fileName, ifstream::binary}; 31 | // auto strings = readCSF>(Input::Range{istreambuf_iterator{file}, istreambuf_iterator{}}, emplacer); 32 | // for(const auto& [label, string] : strings) { 33 | // ... 34 | // } 35 | //This function asssumes that machine's endianness is little-endian, and sizeof(wchar_t) == 2. 36 | template 37 | Container readCSF(Range&& csf, CSFStringEmplacer csfStringEmplacer = CSFStringEmplacer{}); 38 | 39 | //Write CSF from a ForwardIterator Range with an OutputIterator. 40 | //ForwardIterator should be dereferencable into something that after structured binding, 41 | //could be converted to std::string (label) and std::wstring (text) 42 | //Example: 43 | // writeCSF(Input::Range{vector.begin(), vector.end()}, std::ostreambuf_iterator{file}); 44 | //This function asssumes that machine's endianness is little-endian, and sizeof(wchar_t) == 2. 45 | template 46 | OutputIterator writeCSF(ForwardIteratorRange stringsToBeWritten, OutputIterator output); 47 | 48 | /* 49 | CSF file format 50 | 51 | struct CSF { 52 | char[4]; //string " FSC" 53 | uint32_t; //unknown 0x00000003 in little endian, type / version number? 54 | uint32_t; //array size in little endian 55 | uint32_t; //array size in little endian 56 | uint32_t; //unknown zero, probably high-order bytes of array size? 57 | uint32_t; //unknown zero, probably high-order bytes of array size? 58 | LabbeledString[array size]; //array of strings with label 59 | } 60 | 61 | struct LabbeledString { 62 | char[4]; //string " LBL" 63 | uint32_t; //unknown 0x00000001 in little endian, type / version number? 64 | uint32_t; //label size in little endian 65 | char[label size]; //string label name 66 | char[4]; //string " RTS" 67 | uint32_t; //string size 68 | char16_t[string size]; //masked UTF-16 LE string: realChar = 0xFFFF ^ sourceChar 69 | } 70 | */ 71 | 72 | //This output iterator can be used to calculate the required size of an output buffer 73 | struct OutputCounter { 74 | using value_type = void; 75 | using difference_type = void; 76 | using pointer = void; 77 | using reference = void; 78 | using iterator_category = std::output_iterator_tag; 79 | struct NoOpAssignee { 80 | template constexpr void operator=(T&&) const noexcept { } 81 | }; 82 | 83 | NoOpAssignee operator*() const noexcept { return this->assignee; } 84 | OutputCounter& operator++() { ++(this->counter); return *this; } 85 | OutputCounter operator++(int) { auto copy = *this; ++(*this); return copy; } 86 | 87 | int counter = 0; 88 | NoOpAssignee assignee; 89 | }; 90 | 91 | namespace Details { 92 | using namespace std::string_view_literals; 93 | static constexpr auto header = std::string_view {" FSC"sv}; 94 | static constexpr auto zero32Bit = std::string_view {"\x00\x00\x00\x00"sv}; 95 | static constexpr auto lbl = std::string_view {" LBL" "\x01\x00\x00\x00"sv}; 96 | static constexpr auto rts = std::string_view {" RTS"sv}; 97 | 98 | using namespace Input; 99 | 100 | template 101 | std::pair nextString(Range& input) { 102 | 103 | readAndCheckMagic(input, lbl); 104 | 105 | auto labelSize = copyBytes(input); 106 | auto label = std::string{labelSize, '\0', std::string::allocator_type{}}; 107 | copyFixed(input, label.begin(), label.size()); 108 | 109 | readAndCheckMagic(input, rts); 110 | 111 | auto wideCharCount= copyBytes(input); 112 | auto string = std::wstring{wideCharCount, L'\xFFFF', std::wstring::allocator_type{}}; 113 | for(auto& wideChar : string) { wideChar = wideChar xor copyBytes(input); } 114 | 115 | return {std::move(label), std::move(string)}; 116 | } 117 | 118 | template 119 | OutputIterator writeAsBytes(OutputIterator out, const T& data, std::size_t bytes = sizeof(T)) { 120 | return std::copy_n(reinterpret_cast(&data), bytes, out); 121 | } 122 | 123 | template 124 | OutputIterator writeString(const std::string& label, std::wstring string, OutputIterator out) { 125 | 126 | out = writeAsBytes(out, *(lbl.data()), lbl.size()); 127 | out = writeAsBytes(out, label.size()); 128 | out = writeAsBytes(out, *(label.data()), label.size()); 129 | 130 | out = writeAsBytes(out, *(rts.data()), rts.size()); 131 | out = writeAsBytes(out, string.size()); 132 | for(auto& character : string) character = character xor L'\xFFFF'; 133 | return writeAsBytes(out, *(string.data()), sizeof(*string.data()) * string.length()); 134 | } 135 | } 136 | 137 | template 138 | Container readCSF(Range&& csfRange, CSFStringEmplacer csfStringEmplacer) { 139 | using namespace Details; 140 | 141 | readAndCheckMagic(csfRange, header); 142 | [[maybe_unused]] auto version = copyBytes(csfRange); 143 | 144 | auto stringCount = copyBytes(csfRange); 145 | auto labelCount = copyBytes(csfRange); 146 | 147 | if(stringCount != labelCount) { 148 | throw std::invalid_argument("May not be able to correctly parse CSF file: stringCount != labelCount"); 149 | } 150 | 151 | [[maybe_unused]] auto reserved = copyBytes(csfRange); 152 | [[maybe_unused]] auto languageCode = copyBytes(csfRange); 153 | 154 | auto map = Container{}; 155 | 156 | for(auto i = std::uint32_t{0}; i < stringCount; ++i) { 157 | csfStringEmplacer(map, nextString(csfRange)); 158 | } 159 | 160 | //ensure EOF is reached; 161 | if(csfRange.current != csfRange.end) { 162 | throw std::invalid_argument("CSF EOF not reached as expected"); 163 | } 164 | 165 | return map; 166 | } 167 | 168 | template 169 | OutputIterator writeCSF(ForwardIteratorRange stringsToBeWritten, OutputIterator out) { 170 | using namespace Details; 171 | 172 | auto count = std::distance(stringsToBeWritten.current, stringsToBeWritten.end); 173 | 174 | out = writeAsBytes(out, *(header.data()), header.size()); 175 | out = writeAsBytes(out, count); 176 | out = writeAsBytes(out, count); 177 | out = writeAsBytes(out, *(zero32Bit.data()), zero32Bit.size()); 178 | out = writeAsBytes(out, *(zero32Bit.data()), zero32Bit.size()); 179 | 180 | for(const auto& [label, string] : stringsToBeWritten.rangeForLoop()) { 181 | out = writeString(label, string, out); 182 | } 183 | return out; 184 | } 185 | 186 | } 187 | -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "WindowsWrapper.hpp" 6 | #include "UserInterface.hpp" 7 | #include "ReplaysAndMods.hpp" 8 | #include "resource.h" 9 | 10 | std::wstring rebuildArgument(std::wstring_view argument) { 11 | auto result = std::wstring{}; 12 | auto needQuotes = false; 13 | //[n (0 or more) slashes][quote] -> [n * 2 slashes][slash][quote] 14 | for(auto i = std::size_t{0}; i < argument.size(); ++i) { 15 | if(argument[i] == L'\\') { 16 | auto afterSlash = argument.find_first_not_of(L'\\', i); 17 | if(afterSlash < argument.size() and argument[afterSlash] == L'\"') { 18 | result += L'\\'; 19 | } 20 | } 21 | else if(argument[i] == L'\"') { 22 | result += L'\\'; 23 | } 24 | else if(argument[i] == L' ' or argument[i] == L'\t') { 25 | needQuotes = true; 26 | } 27 | result += argument[i]; 28 | } 29 | 30 | if(needQuotes) { 31 | result = L'\"' + result + L'\"'; 32 | } 33 | 34 | return result; 35 | }; 36 | 37 | std::vector getArguments(const std::wstring& commandLine) { 38 | if(commandLine.empty()) { 39 | return {}; 40 | } 41 | 42 | using namespace Windows; 43 | 44 | auto count = 0; 45 | auto arguments = LocalMemory { 46 | CommandLineToArgvW(commandLine.c_str(), &count) 47 | >> checkWin32Result("CommandLineToArgvW", errorValue, nullptr) 48 | }; 49 | 50 | auto result = std::vector {}; 51 | for(auto i = 0; i < count; ++i) { 52 | result.emplace_back(arguments[i]); 53 | } 54 | 55 | return result; 56 | } 57 | 58 | std::wstring parseItemInCommandLine(const std::wstring& item) { 59 | using namespace Windows; 60 | auto count = 0; 61 | auto parsed = LocalMemory { 62 | CommandLineToArgvW(item.c_str(), &count) 63 | >> checkWin32Result("CommandLineToArgvW", errorValue, nullptr) 64 | }; 65 | if(count != 1) { 66 | throw std::runtime_error("parseItemInCommandLine CommandLineToArgvW -> count != 1"); 67 | } 68 | return parsed[0]; 69 | } 70 | 71 | enum class ExtractMode { 72 | extractName, 73 | extractValue, 74 | }; 75 | 76 | enum class ActionAfterExtract { 77 | keepItem, 78 | removeItem, 79 | }; 80 | 81 | template 82 | std::optional extractArgument(std::vector& arguments, Predicate predicate, 83 | ExtractMode extractMode, ActionAfterExtract actionAfterExtract = ActionAfterExtract::keepItem) { 84 | 85 | auto position = std::find_if(std::begin(arguments), std::end(arguments), predicate); 86 | auto valuePosition = position; 87 | if(position == std::end(arguments)) { 88 | return std::nullopt; 89 | } 90 | 91 | if(extractMode == ExtractMode::extractValue) { 92 | valuePosition = std::next(valuePosition); 93 | } 94 | 95 | auto value = std::optional {}; 96 | 97 | if(valuePosition != std::end(arguments)) { 98 | value = *valuePosition; 99 | } 100 | 101 | if(actionAfterExtract == ActionAfterExtract::removeItem) { 102 | if(valuePosition != position and valuePosition != std::end(arguments)) { 103 | arguments.erase(valuePosition); 104 | } 105 | arguments.erase(position); 106 | } 107 | 108 | return value; 109 | } 110 | 111 | std::optional parseInt(const std::optional& string) { 112 | if(not string.has_value()) { 113 | return std::nullopt; 114 | } 115 | try { 116 | return std::stoi(string.value()); 117 | } 118 | catch(...) { 119 | return std::nullopt; 120 | } 121 | } 122 | 123 | std::optional getItemFromSkudef(const std::wstring& skudefPath, std::wstring_view itemName) { 124 | auto fileBuffer = readEntireFile(Windows::createFile(skudefPath, GENERIC_READ, FILE_SHARE_READ|FILE_SHARE_WRITE, OPEN_EXISTING).get()); 125 | auto skudefContent = Windows::toWide({fileBuffer.data(), fileBuffer.size()}); 126 | auto position = insensitiveSearch(std::begin(skudefContent), std::end(skudefContent), 127 | std::begin(itemName), std::end(itemName)) - std::begin(skudefContent); 128 | if(static_cast(position) >= skudefContent.size()) { 129 | return std::nullopt; 130 | } 131 | 132 | auto itemBegin = skudefContent.find_first_not_of(L' ', position + itemName.size()); 133 | auto itemEnd = skudefContent.find_first_of(L"\r\n", itemBegin); 134 | if(itemBegin >= skudefContent.size()) { 135 | return std::nullopt; 136 | } 137 | 138 | return skudefContent.substr(itemBegin, itemEnd - itemBegin); 139 | } 140 | 141 | HWND findRA3Window(HANDLE processHandle) { 142 | auto processIDAndWindow = std::pair{GetProcessId(processHandle), HWND{}}; 143 | struct EnumWindowsCallback { 144 | static BOOL CALLBACK function(HWND window, LPARAM dataAddress) { 145 | auto processID = DWORD{}; 146 | GetWindowThreadProcessId(window, &processID); 147 | auto& data = *reinterpret_cast(dataAddress); 148 | if(data.first != processID) { 149 | return TRUE; 150 | } 151 | if((GetWindow(window, GW_OWNER) != nullptr) or (not IsWindowVisible(window))) { 152 | return TRUE; 153 | } 154 | auto classNameBuffer = std::array{}; 155 | using namespace std::literals; 156 | auto constexpr ra3ClassName = L"41DAF790-16F5-4881-8754-59FD8CF3B8D2"sv; 157 | if(not GetClassNameW(window, classNameBuffer.data(), classNameBuffer.size())) { 158 | return TRUE; 159 | } 160 | classNameBuffer[0] = ra3ClassName[0]; // 我的红警3魔改过( 161 | if(classNameBuffer.data() != ra3ClassName) { 162 | return TRUE; 163 | } 164 | // 这是红警3进程的窗口,完美匹配,不用继续找了 165 | data.second = window; 166 | return FALSE; 167 | } 168 | }; 169 | EnumWindows(&EnumWindowsCallback::function, reinterpret_cast(&processIDAndWindow)); 170 | return processIDAndWindow.second; 171 | } 172 | 173 | int main() { 174 | 175 | //default language data, 176 | //gettext will use default embbed ra3bar - english language pack, if available 177 | auto languageData = LanguageData{}; 178 | 179 | try { 180 | using namespace Windows; 181 | auto pathLength = GetCurrentDirectoryW(0, nullptr) >> checkWin32Result("GetCurrentDirectoryW", errorValue, 0); 182 | auto ra3Path = std::wstring{pathLength, L'\0', std::wstring::allocator_type{}}; 183 | GetCurrentDirectoryW(ra3Path.size(), ra3Path.data()) >> checkWin32Result("GetCurrentDirectoryW", errorValue, 0); 184 | ra3Path.erase(std::min(ra3Path.size(), ra3Path.find(L'\0'))); 185 | ra3Path.back() == L'\\' ? static_cast(NULL) : ra3Path.push_back(L'\\'); 186 | 187 | if(getSkudefs(ra3Path, L"*").empty()) { 188 | try { 189 | ra3Path = getRegistryString(getRa3RegistryKey(HKEY_LOCAL_MACHINE).get(), L"Install Dir"); 190 | } 191 | catch(...) { 192 | MessageBoxW(nullptr, L"Game installation not found. You can try to put this program inside your RA3 folder.", nullptr, MB_TOPMOST|MB_ICONEXCLAMATION); 193 | return 1; 194 | } 195 | ra3Path.back() == L'\\' ? static_cast(NULL) : ra3Path.push_back(L'\\'); 196 | } 197 | 198 | languageData = loadPreferredLanguageData(ra3Path); 199 | 200 | auto runControlCenterWhenNeeded = [&ra3Path, &languageData](bool canRun, const std::wstring& userOptions, HBITMAP customBackground) { 201 | if(not canRun) { 202 | return std::optional {}; 203 | } 204 | return runControlCenter(ra3Path, userOptions, languageData, customBackground); 205 | }; 206 | 207 | 208 | auto stringEqual = [](std::wstring_view a, std::wstring_view b) { 209 | return insensitiveEqual(std::begin(a), std::end(a), std::begin(b), std::end(b)); 210 | }; 211 | auto checkWithString = [stringEqual](std::wstring_view s) { return std::bind(stringEqual, s, std::placeholders::_1); }; 212 | 213 | auto getInitialUserOptions = [runControlCenterWhenNeeded, checkWithString] { 214 | auto userOptions = std::wstring{GetCommandLineW()}; 215 | auto initialArguments = getArguments(userOptions); 216 | userOptions.clear(); 217 | for(auto i = std::size_t{1}; i < initialArguments.size(); ++i) { 218 | userOptions += L' '; 219 | userOptions += rebuildArgument(initialArguments[i]); 220 | } 221 | auto options = std::optional{{LaunchOptions::noFile, {}, userOptions}}; 222 | auto uiFlag = extractArgument(initialArguments, checkWithString(L"-ui"), 223 | ExtractMode::extractName, ActionAfterExtract::keepItem).has_value(); 224 | if(uiFlag) { 225 | options = runControlCenterWhenNeeded(uiFlag, userOptions, nullptr); 226 | } 227 | return std::pair{std::move(options), uiFlag}; 228 | }; 229 | 230 | auto arbg = loadImage(GetModuleHandle(nullptr), AR_BACKGROUND); 231 | auto customBackground = HBITMAP{nullptr}; 232 | 233 | auto userOptions = std::wstring{}; 234 | 235 | for(auto [options, uiFlag] = getInitialUserOptions(); 236 | options.has_value(); 237 | options = runControlCenterWhenNeeded(uiFlag, options.value().extraCommandLine, customBackground)) { 238 | customBackground = nullptr; 239 | 240 | const auto& [loadFileType, loadFilePath, extraCommandLine] = options.value(); 241 | auto arguments = getArguments(extraCommandLine); 242 | 243 | auto getAndRemoveArgument = [&arguments](auto predicate, ExtractMode mode) { 244 | return extractArgument(arguments, predicate, mode, ActionAfterExtract::removeItem); 245 | }; 246 | auto getArgument = [checkWithString, &arguments](std::wstring_view name, ExtractMode mode) { 247 | return extractArgument(arguments, checkWithString(name), mode, ActionAfterExtract::keepItem); 248 | }; 249 | 250 | auto ev = getAndRemoveArgument(checkWithString(L"-runver"), ExtractMode::extractValue); 251 | auto em = getAndRemoveArgument(checkWithString(L"-modConfig"), ExtractMode::extractValue); 252 | auto er = getAndRemoveArgument(checkWithString(L"-replayGame"), ExtractMode::extractValue); 253 | auto ex = parseInt(getAndRemoveArgument(checkWithString(L"-x"), ExtractMode::extractValue)); 254 | auto ey = parseInt(getAndRemoveArgument(checkWithString(L"-y"), ExtractMode::extractValue)); 255 | auto et = getAndRemoveArgument(checkWithString(L"-allowAlwaysOnTop"), ExtractMode::extractName); 256 | auto windowed = getArgument(L"-win", ExtractMode::extractName); 257 | auto fullscreen = getArgument(L"-fullscreen", ExtractMode::extractName); 258 | auto resolutionX = parseInt(getArgument(L"-xres", ExtractMode::extractValue)); 259 | auto resolutionY = parseInt(getArgument(L"-yres", ExtractMode::extractValue)); 260 | if(not er.has_value()) { 261 | er = getAndRemoveArgument(checkWithString(L"-file"), ExtractMode::extractValue); 262 | } 263 | if(not er.has_value()) { 264 | auto predicate = [stringEqual](const std::wstring& argument) { 265 | namespace Replays = ReplaysAndMods; 266 | if(argument.size() < Replays::replayExtension.size()) { 267 | return false; 268 | } 269 | auto extensionBegin = argument.size() - Replays::replayExtension.size(); 270 | if(not stringEqual(Replays::replayExtension, std::wstring_view{argument}.substr(extensionBegin))) { 271 | return false; 272 | } 273 | try { 274 | auto file = Windows::createFile(argument, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, OPEN_EXISTING); 275 | auto head = readFile(file.get(), Replays::replayHeaderMagic.size()); 276 | if(not std::equal(std::begin(head), std::end(head), 277 | std::begin(Replays::replayHeaderMagic), std::end(Replays::replayHeaderMagic))) { 278 | return false; 279 | } 280 | } 281 | catch(...) { 282 | return false; 283 | } 284 | return true; 285 | }; 286 | er = extractArgument(arguments, predicate, ExtractMode::extractName, ActionAfterExtract::keepItem); 287 | } 288 | 289 | if(loadFileType == LaunchOptions::mod) { 290 | er = std::nullopt; 291 | em = loadFilePath; 292 | } 293 | 294 | if(loadFileType == LaunchOptions::replay) { 295 | ev = std::nullopt; 296 | em = std::nullopt; 297 | er = loadFilePath; 298 | } 299 | 300 | if(er.has_value()) { 301 | auto replayDetails = ReplaysAndMods::ReplayDetails{}; 302 | try { 303 | replayDetails = ReplaysAndMods::getReplayDetails(er.value()); 304 | } 305 | catch(...) { 306 | notifyIsNotReplay(languageData); 307 | continue; 308 | } 309 | 310 | auto [version, subVersion] = replayDetails.gameVersion; 311 | ev = std::to_wstring(version) + L"." + std::to_wstring(subVersion); 312 | 313 | em.reset(); 314 | auto replayMods = ReplaysAndMods::getModSkudefPathsFromReplay(replayDetails); 315 | if(replayMods.has_value()) { 316 | if(replayMods->empty()) { 317 | notifyReplayModNotFound(languageData); 318 | continue; 319 | } 320 | else if(replayMods->size() > 1) { 321 | notifyReplayModAmbiguity(languageData); 322 | continue; 323 | } 324 | em = replayMods->front(); 325 | } 326 | 327 | auto placeholder = std::optional {std::nullopt}; 328 | if(not replayDetails.finalTimeCode.has_value()) { 329 | std::swap(placeholder, er); 330 | if(askUserIfFixReplay(placeholder.value(), languageData)) { 331 | try { 332 | fixReplayByFileName(placeholder.value()); 333 | std::swap(er, placeholder); 334 | } 335 | catch(const std::exception& error) { 336 | notifyFixReplayFailed(error, languageData); 337 | } 338 | } 339 | } 340 | if((er.has_value()) and (replayDetails.hasCommentator == false)) { 341 | std::swap(placeholder, er); 342 | notifyNoCommentatorAvailable(languageData); 343 | } 344 | } 345 | 346 | if(em.has_value()) { 347 | const auto& modSkudef = em.value(); 348 | if(not fileExists(modSkudef)) { 349 | notifyModNotFound(languageData); 350 | continue; 351 | } 352 | 353 | if(auto gameVersionForMod = getItemFromSkudef(modSkudef, L"mod-game"); 354 | gameVersionForMod.has_value()) { 355 | ev = gameVersionForMod; 356 | } 357 | 358 | constexpr auto arString = std::wstring_view{L"Armor Rush"}; 359 | if(insensitiveSearch(std::begin(modSkudef), std::end(modSkudef), 360 | std::begin(arString), std::end(arString)) != std::end(modSkudef)) { 361 | customBackground = arbg.get(); 362 | } 363 | } 364 | 365 | auto allSkudefs = getSkudefs(ra3Path, languageData.languageName); 366 | auto skudefMax = std::max_element(std::begin(allSkudefs), std::end(allSkudefs), [](const std::wstring& a, const std::wstring& b) { 367 | auto versionA = a.substr(a.rfind(L'_') + 1); 368 | auto versionB = b.substr(b.rfind(L'_') + 1); 369 | auto subVersionA = versionA.substr(versionA.find(L'.') + 1); 370 | auto subVersionB = versionB.substr(versionB.find(L'.') + 1); 371 | using std::pair; 372 | return pair{parseInt(versionA), parseInt(subVersionA)} < pair{parseInt(versionB), parseInt(subVersionB)}; 373 | }); 374 | if(skudefMax == std::end(allSkudefs)) { 375 | notifyGameVersionNotFound(L"?", languageData); 376 | continue; 377 | } 378 | 379 | auto gameConfig = ra3Path + *skudefMax; 380 | if(ev.has_value()) { 381 | const auto& version = ev.value(); 382 | auto skudef = std::find_if(std::begin(allSkudefs), std::end(allSkudefs), [&version](const std::wstring& fileName) { 383 | return fileName.find(version, fileName.rfind('_')) != fileName.npos; 384 | }); 385 | if(skudef == std::end(allSkudefs)) { 386 | notifyGameVersionNotFound(version, languageData); 387 | continue; 388 | } 389 | gameConfig = ra3Path + *skudef; 390 | } 391 | 392 | auto configArgument = L" -config " + rebuildArgument(gameConfig); 393 | auto modArgument = std::wstring{}; 394 | auto replayArgument = std::wstring{}; 395 | if(em.has_value()) { 396 | modArgument = L" -modConfig " + rebuildArgument(em.value()); 397 | } 398 | if(er.has_value()) { 399 | replayArgument = L" -replayGame " + rebuildArgument(er.value()); 400 | } 401 | 402 | auto otherArguments = std::wstring{}; 403 | for(const auto& argument : arguments) { 404 | otherArguments += L' '; 405 | otherArguments += rebuildArgument(argument); 406 | } 407 | 408 | auto gameExe = getItemFromSkudef(gameConfig, L"set-exe").value(); 409 | 410 | if(displaySplashScreen(ra3Path, languageData) == SplashScreenResult::clicked) { 411 | uiFlag = true; 412 | continue; 413 | } 414 | 415 | auto finalArguments = ra3Path + gameExe + otherArguments + modArgument + replayArgument + configArgument; 416 | auto game = createProcess(finalArguments, ra3Path.c_str()); 417 | // 假如游戏以窗口化模式启动,等待游戏窗口出现后,可以自动调整窗口位置 418 | if(windowed.has_value()) { 419 | constexpr auto interval = 499; 420 | while(WaitForSingleObject(game.get(), interval) == WAIT_TIMEOUT) { 421 | if(WaitForInputIdle(game.get(), 1) != 0) { 422 | continue; 423 | } 424 | auto gameWindow = findRA3Window(game.get()); 425 | if(gameWindow == nullptr) { 426 | continue; 427 | } 428 | #ifndef SMTO_ERRORONEXIT // workaround for some outdated version of mingw 429 | #define SMTO_ERRORONEXIT 0x0020 430 | #endif 431 | if(SendMessageTimeoutW(gameWindow, WM_NULL, 0, 0, SMTO_ERRORONEXIT, interval, nullptr) == 0) { 432 | continue; 433 | } 434 | auto rect = getWindowRect(gameWindow); 435 | 436 | // 在全屏窗口化模式下,自动居中窗口 437 | if(fullscreen.has_value()) { 438 | auto desktop = getWindowRect(GetDesktopWindow()); 439 | if(resolutionX.has_value()) { 440 | rect.left = (rectWidth(desktop) - resolutionX.value()) / 2; 441 | } 442 | if(resolutionY.has_value()) { 443 | rect.top = (rectHeight(desktop) - resolutionY.value()) / 2; 444 | } 445 | } 446 | 447 | // 假如玩家在命令行参数里指定了窗口位置,就将窗口移动到指定位置 448 | if(ex.has_value()) { 449 | rect.left = ex.value(); 450 | } 451 | if(ey.has_value()) { 452 | rect.top = ey.value(); 453 | } 454 | 455 | auto zOrderFlag = et.has_value() ? SWP_NOZORDER : 0; 456 | SetWindowPos(gameWindow, HWND_NOTOPMOST, rect.left, rect.top, 0, 0, zOrderFlag|SWP_ASYNCWINDOWPOS|SWP_NOSIZE); 457 | break; 458 | } 459 | } 460 | 461 | // 等待游戏结束 462 | WaitForSingleObject(game.get(), INFINITE) >> checkWin32Result("WaitForSingleOnject", errorValue, WAIT_FAILED); 463 | auto exitCode = DWORD{0}; 464 | GetExitCodeProcess(game.get(), &exitCode) >> checkWin32Result("GetExitCodeProcess", errorValue, false); 465 | if(exitCode == 123456789) { 466 | notifyCantUpdate(languageData); 467 | } 468 | } 469 | } 470 | catch(std::exception& exception) { 471 | displayErrorMessage(exception, languageData); 472 | return 1; 473 | } 474 | return 0; 475 | } 476 | 477 | -------------------------------------------------------------------------------- /ReplaysAndMods.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "WindowsWrapper.hpp" 8 | #include 9 | #include 10 | #include "Input.hpp" 11 | #include "Common.hpp" 12 | 13 | //Common.hpp 14 | template 15 | void readFile(HANDLE file, std::vector& buffer, std::size_t count); 16 | template 17 | std::vector readFile(HANDLE file, std::size_t count); 18 | template 19 | bool insensitiveEqual(InputIterator1 begin1, InputIterator1 end1, InputIterator2 begin2, InputIterator2 end2); 20 | 21 | namespace ReplaysAndMods { 22 | 23 | struct ReplayDetails { 24 | std::wstring fullPath; 25 | std::wstring replayName; 26 | std::optional finalTimeCode; 27 | std::wstring modName; 28 | std::wstring modVersion; 29 | std::pair gameVersion; 30 | std::uint32_t timeStamp; 31 | std::wstring title; 32 | std::wstring map; 33 | std::vector players; 34 | std::wstring description; 35 | bool hasCommentator; 36 | }; 37 | 38 | struct ModDetails { 39 | std::wstring fullPath; 40 | std::wstring modName; 41 | std::wstring version; 42 | }; 43 | 44 | 45 | template 46 | ReplayDetails parseReplayHeader(Range&& replay); 47 | template 48 | std::vector fixReplay(Range&& replay); 49 | template 50 | std::optional getFinalTimeCodeFromLastBytes(Range&& terminatorAndFooter); 51 | inline ReplayDetails getReplayDetails(const std::wstring& replayFullPath); 52 | inline std::vector getAllReplayDetails(); 53 | inline std::vector getModSkudefs(); 54 | inline std::wstring concatenateWithReplayFolder(std::wstring_view replay); 55 | inline std::wstring concatenateWithModRootFolder(std::wstring_view mod); 56 | inline std::optional> getModSkudefPathsFromReplay(const ReplayDetails& replay); 57 | 58 | inline const auto replayExtension = std::wstring {L".ra3replay"}; 59 | inline const auto skudefExtension = std::wstring {L".skudef"}; 60 | inline constexpr auto replayHeaderMagic = std::string_view {"RA3 REPLAY HEADER"}; 61 | namespace Internal { 62 | using namespace Windows; 63 | using namespace Input; 64 | using namespace std::string_view_literals; 65 | 66 | inline constexpr auto cncMagic = std::string_view {"CNC3RPL\0"sv}; 67 | inline constexpr auto terminator = std::string_view {"\xFF\xFF\xFF\x7F"sv}; 68 | inline constexpr auto footerMagic = std::string_view {"RA3 REPLAY FOOTER"sv}; 69 | inline constexpr auto prePlainTextPadding = 31; 70 | 71 | inline const std::wstring wildcardAny = L"*"; 72 | 73 | 74 | inline std::wstring getRA3UserFolder(std::wstring_view subDirectory) { 75 | auto userDataLeafName = std::wstring{L"Red Alert 3"}; 76 | 77 | try { 78 | auto redAlert3Registry = openRegistryKey(HKEY_LOCAL_MACHINE, L"Software\\Electronic Arts\\Electronic Arts\\Red Alert 3"); 79 | userDataLeafName = getRegistryString(redAlert3Registry.get(), L"UserDataLeafName"); 80 | } 81 | catch(...) { } 82 | 83 | auto ra3UserPathName = std::wstring{MAX_PATH, {}, std::wstring::allocator_type{}}; 84 | auto getPathResult = 85 | SHGetFolderPathW(nullptr, CSIDL_MYDOCUMENTS|CSIDL_FLAG_CREATE, nullptr, SHGFP_TYPE_CURRENT, ra3UserPathName.data()); 86 | if(getPathResult != S_OK) { 87 | throw std::runtime_error("SHGetFolderPathAndSubDir failed, error code " + std::to_string(getPathResult)); 88 | } 89 | ra3UserPathName.resize(std::min(ra3UserPathName.size(), ra3UserPathName.find('\0'))); 90 | 91 | appendToFolder(ra3UserPathName, userDataLeafName); 92 | if(not isDirectory(ra3UserPathName)) { 93 | CreateDirectoryW(ra3UserPathName.c_str(), nullptr) >> checkWin32Result("CreateDirectoryW", errorValue, false); 94 | } 95 | 96 | appendToFolder(ra3UserPathName, subDirectory); 97 | if(not isDirectory(ra3UserPathName)) { 98 | CreateDirectoryW(ra3UserPathName.c_str(), nullptr) >> checkWin32Result("CreateDirectoryW", errorValue, false); 99 | } 100 | 101 | return ra3UserPathName; 102 | } 103 | 104 | template 105 | std::wstring readNullTerminatedWideString(Range& input) { 106 | auto result = std::wstring {}; 107 | do { 108 | result += copyBytes(input); 109 | } 110 | while(result.back() != L'\0'); 111 | result.pop_back(); 112 | return result; 113 | } 114 | } 115 | 116 | std::wstring concatenateWithReplayFolder(std::wstring_view replay) { 117 | using namespace Internal; 118 | auto replaysFolder = std::wstring{L"Replays"}; 119 | 120 | try { 121 | auto redAlert3Registry = openRegistryKey(HKEY_LOCAL_MACHINE, L"Software\\Electronic Arts\\Electronic Arts\\Red Alert 3"); 122 | replaysFolder = getRegistryString(redAlert3Registry.get(), L"ReplayFolderName"); 123 | } 124 | catch(...) { } 125 | 126 | return concatenatePath(getRA3UserFolder(replaysFolder), replay); 127 | } 128 | 129 | std::wstring concatenateWithModRootFolder(std::wstring_view mod) { 130 | using namespace Internal; 131 | return concatenatePath(getRA3UserFolder(L"Mods"), mod); 132 | } 133 | 134 | template 135 | ReplayDetails parseReplayHeader(Range&& replay) { 136 | using namespace Internal; 137 | 138 | readAndCheckMagic(replay, replayHeaderMagic); 139 | 140 | auto hNumber = std::to_integer(copyBytes(replay)); 141 | 142 | auto majorVersion = copyBytes(replay); 143 | auto minorVersion = copyBytes(replay); 144 | auto gameVersion = std::pair{majorVersion, minorVersion}; 145 | 146 | ignore(replay); //build major 147 | ignore(replay); //build minor 148 | ignore(replay); //commentary track flag? 149 | ignore(replay); //zero 150 | 151 | auto title = readNullTerminatedWideString(replay); 152 | auto description = readNullTerminatedWideString(replay); 153 | auto mapName = readNullTerminatedWideString(replay); 154 | auto mapID = readNullTerminatedWideString(replay); 155 | 156 | auto numberOfPlayers = std::to_integer(copyBytes(replay)); 157 | 158 | auto playerNames = std::vector {numberOfPlayers + 1, {}, std::vector::allocator_type{}}; 159 | for(auto& playerName : playerNames) { 160 | ignore(replay); //skip player id 161 | playerName = readNullTerminatedWideString(replay); 162 | if(hNumber == 0x05u) { 163 | ignore(replay); //skip team number 164 | } 165 | } 166 | playerNames.pop_back(); 167 | 168 | auto offset = copyBytes(replay); 169 | 170 | if(copyBytes(replay) != cncMagic.size()) { 171 | throw std::invalid_argument("incorrect CNC3RPL magic length"); 172 | } 173 | readAndCheckMagic(replay, cncMagic); 174 | 175 | constexpr auto modInfoSize = std::size_t{22}; 176 | auto modInfo = std::string{modInfoSize, {}, std::string::allocator_type{}}; 177 | copyFixed(replay, modInfo.begin(), modInfo.size()); 178 | auto modVersion = modInfo.substr(modInfo.find_last_of('\0', modInfo.find_last_not_of('\0')) + 1); 179 | modInfo.resize(std::min(modInfo.size(), modInfo.find('\0'))); 180 | modVersion.resize(std::min(modVersion.size(), modVersion.find('\0'))); 181 | 182 | auto timeStamp = copyBytes(replay); 183 | 184 | ignore(replay, prePlainTextPadding); 185 | auto plainTextLength = copyBytes(replay); 186 | auto plainText = std::string{plainTextLength, {}, std::string::allocator_type{}}; 187 | copyFixed(replay, plainText.begin(), plainTextLength); 188 | 189 | auto hasCommentator = (plainText.rfind(":Hpost Commentator") != plainText.npos); 190 | 191 | auto afterOffset = cncMagic.size() + modInfo.size() + sizeof(timeStamp) 192 | + prePlainTextPadding + sizeof(plainTextLength) + plainText.size(); 193 | ignore(replay, offset - afterOffset); 194 | 195 | using std::move; 196 | return {{}, {}, std::nullopt, toWide(modInfo), toWide(modVersion), gameVersion, timeStamp, 197 | move(title), move(mapName), move(playerNames), move(description), hasCommentator}; 198 | } 199 | 200 | template 201 | std::vector fixReplay(Range&& replay) { 202 | using namespace Internal; 203 | auto enlargeAndGetIterator = [](std::vector& buffer, std::size_t numberOfNewBytesToAdd) { 204 | std::size_t oldSize = buffer.size(); 205 | buffer.resize(oldSize + numberOfNewBytesToAdd); 206 | return std::next(std::begin(buffer), oldSize); 207 | }; 208 | 209 | auto replayData = std::vector {}; 210 | 211 | readAndCheckMagic(replay, replayHeaderMagic); 212 | std::copy(std::begin(replayHeaderMagic), std::end(replayHeaderMagic), 213 | enlargeAndGetIterator(replayData, replayHeaderMagic.size())); 214 | 215 | auto hNumber = copyBytes(replay); 216 | replayData.emplace_back(hNumber); 217 | 218 | auto versionNumbersAndFlagsSize = sizeof(std::uint32_t) * 4 + sizeof(char) * 2; 219 | copyFixed(replay, enlargeAndGetIterator(replayData, versionNumbersAndFlagsSize), versionNumbersAndFlagsSize); 220 | 221 | auto copyWideStringAsCharArray = [enlargeAndGetIterator](std::wstring_view string, std::vector& buffer) { 222 | auto begin = reinterpret_cast(string.data()); 223 | auto bytesToCopy = string.size() * sizeof(*string.data()); 224 | return std::copy_n(begin, bytesToCopy, enlargeAndGetIterator(buffer, bytesToCopy)); 225 | }; 226 | 227 | for(auto i = 0; i < 4; ++i) { 228 | copyWideStringAsCharArray(readNullTerminatedWideString(replay) + L'\0', replayData); 229 | } 230 | 231 | auto numberOfPlayers = copyBytes(replay); 232 | replayData.emplace_back(static_cast(numberOfPlayers)); 233 | 234 | for(auto i = 0; i < numberOfPlayers + 1; ++i) { 235 | copyFixed(replay, enlargeAndGetIterator(replayData, sizeof(std::uint32_t)), sizeof(std::uint32_t)); 236 | copyWideStringAsCharArray(readNullTerminatedWideString(replay) + L'\0', replayData); 237 | if(hNumber == 0x05) { 238 | replayData.emplace_back(copyBytes(replay)); 239 | } 240 | } 241 | 242 | auto offset = copyBytes(replay); 243 | std::copy_n(reinterpret_cast(&offset), sizeof(offset), 244 | enlargeAndGetIterator(replayData, sizeof(offset))); 245 | 246 | copyFixed(replay, enlargeAndGetIterator(replayData, sizeof(std::uint32_t)), sizeof(std::uint32_t)); 247 | readAndCheckMagic(replay, cncMagic); 248 | std::copy(std::begin(cncMagic), std::end(cncMagic), enlargeAndGetIterator(replayData, cncMagic.size())); 249 | 250 | copyFixed(replay, enlargeAndGetIterator(replayData, offset - cncMagic.size()), offset - cncMagic.size()); 251 | 252 | auto lastTimeCode = std::array {}; 253 | try { 254 | 255 | while(true) { 256 | auto chunk = std::vector {}; 257 | 258 | auto chunkTimeCode = decltype(lastTimeCode) {}; 259 | copyFixed(replay, std::begin(chunkTimeCode), chunkTimeCode.size()); 260 | if(std::equal(std::begin(chunkTimeCode), std::end(chunkTimeCode), 261 | std::begin(terminator), std::end(terminator))) { 262 | break; 263 | } 264 | 265 | std::copy(std::begin(chunkTimeCode), std::end(chunkTimeCode), 266 | enlargeAndGetIterator(chunk, chunkTimeCode.size())); 267 | 268 | chunk.emplace_back(copyBytes(replay)); //chunk type 269 | 270 | auto chunkSize = copyBytes(replay); 271 | std::copy_n(reinterpret_cast(&chunkSize), sizeof(chunkSize), 272 | enlargeAndGetIterator(chunk, sizeof(chunkSize))); 273 | copyFixed(replay, enlargeAndGetIterator(chunk, chunkSize), chunkSize); 274 | 275 | constexpr auto zeroes = std::string_view{"\0\0\0\0"sv}; 276 | readAndCheckMagic(replay, zeroes); 277 | std::copy(std::begin(zeroes), std::end(zeroes), enlargeAndGetIterator(chunk, zeroes.size())); 278 | 279 | lastTimeCode = chunkTimeCode; 280 | std::copy(std::begin(chunk), std::end(chunk), enlargeAndGetIterator(replayData, chunk.size())); 281 | } 282 | 283 | auto footer = std::vector {}; 284 | 285 | std::copy(std::begin(terminator), std::end(terminator), enlargeAndGetIterator(footer, terminator.size())); 286 | std::copy(replay.current, replay.end, std::back_inserter(footer)); 287 | 288 | auto finalTimeCode = getFinalTimeCodeFromLastBytes(Range{std::begin(footer), std::end(footer)}).value(); 289 | std::copy_n(reinterpret_cast(&finalTimeCode), lastTimeCode.size(), std::begin(lastTimeCode)); 290 | 291 | std::copy(std::begin(footer), std::end(footer), enlargeAndGetIterator(replayData, footer.size())); 292 | } 293 | catch(...) { 294 | auto myFooter = std::vector {}; 295 | 296 | std::copy(std::begin(terminator), std::end(terminator), enlargeAndGetIterator(myFooter, terminator.size())); 297 | std::copy(std::begin(footerMagic), std::end(footerMagic), enlargeAndGetIterator(myFooter, footerMagic.size())); 298 | std::copy(std::begin(lastTimeCode), std::end(lastTimeCode), enlargeAndGetIterator(myFooter, lastTimeCode.size())); 299 | constexpr auto finalData = std::string_view{"\x02\x1A\x00\x00\x00"sv}; 300 | std::copy(std::begin(finalData), std::end(finalData), enlargeAndGetIterator(myFooter, finalData.size())); 301 | auto footerLength = myFooter.size(); 302 | std::copy_n(reinterpret_cast(&footerLength), sizeof(footerLength), enlargeAndGetIterator(myFooter, sizeof(footerLength))); 303 | 304 | std::copy(std::begin(myFooter), std::end(myFooter), enlargeAndGetIterator(replayData, myFooter.size())); 305 | } 306 | 307 | return replayData; 308 | } 309 | 310 | template 311 | std::optional getFinalTimeCodeFromLastBytes(Range&& terminatorAndFooter) { 312 | using namespace Internal; 313 | try { 314 | readAndCheckMagic(terminatorAndFooter, terminator); 315 | readAndCheckMagic(terminatorAndFooter, footerMagic); 316 | auto finalTimeCode = copyBytes(terminatorAndFooter); 317 | 318 | auto remainedBytes = std::vector {}; 319 | std::copy(terminatorAndFooter.current, terminatorAndFooter.end, std::back_inserter(remainedBytes)); 320 | if(remainedBytes.size() < sizeof(std::uint32_t)) { 321 | throw std::out_of_range("remainedBytes.size() < sizeof(std::uint32_t)"); 322 | } 323 | auto footerLengthRange = Range{std::end(remainedBytes) - sizeof(std::uint32_t), std::end(remainedBytes)}; 324 | auto footerLength = copyBytes(footerLengthRange); 325 | if((footerLength - footerMagic.size() - sizeof(finalTimeCode)) != remainedBytes.size()) { 326 | throw std::invalid_argument("Incorrect footer length"); 327 | } 328 | return finalTimeCode; 329 | } 330 | catch(...) { } 331 | return std::nullopt; 332 | } 333 | 334 | ReplayDetails getReplayDetails(const std::wstring& replayFullPath) { 335 | using namespace Internal; 336 | auto file = createFile(replayFullPath, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, OPEN_EXISTING); 337 | auto buffer = std::vector {}; 338 | auto replayDetails = ReplayDetails{}; 339 | while(true) { 340 | auto fileSize = getFileSize(file.get()); 341 | readFile(file.get(), buffer, std::clamp(buffer.size() * 2, 1024, fileSize - buffer.size())); 342 | try { 343 | replayDetails = parseReplayHeader(Range{std::begin(buffer), std::end(buffer)}); 344 | break; 345 | } 346 | catch(const RangeException&) { 347 | if(buffer.size() >= fileSize) { 348 | throw; 349 | } 350 | } 351 | } 352 | replayDetails.fullPath = std::move(replayFullPath); 353 | 354 | auto footerLength = std::uint32_t{}; 355 | setFilePointer(file.get(), -static_cast(sizeof(footerLength)), FILE_END); 356 | auto lengthBuffer = readFile(file.get(), sizeof(std::uint32_t)); 357 | std::copy(std::begin(lengthBuffer), std::end(lengthBuffer), reinterpret_cast(&footerLength)); 358 | 359 | try { 360 | auto terminatorAndFooterLength = footerLength + sizeof(std::uint32_t); 361 | setFilePointer(file.get(), -static_cast(terminatorAndFooterLength), FILE_END); 362 | auto lastBytes = readFile(file.get(), terminatorAndFooterLength); 363 | replayDetails.finalTimeCode = getFinalTimeCodeFromLastBytes(Range{std::begin(lastBytes), std::end(lastBytes)}); 364 | } 365 | catch(...) { } 366 | 367 | return replayDetails; 368 | } 369 | 370 | std::vector getAllReplayDetails() { 371 | using namespace Internal; 372 | auto replayPath = concatenateWithReplayFolder({}); 373 | auto allReplays = findAllMatchingFiles(concatenatePath(replayPath, wildcardAny + replayExtension)); 374 | auto replayDetails = std::vector {}; 375 | for(auto& fileName : allReplays) { 376 | try { 377 | replayDetails.emplace_back(getReplayDetails(concatenatePath(replayPath, fileName))); 378 | replayDetails.back().replayName = std::move(fileName); 379 | } 380 | catch(...) { /* simply skip unparsable replays */ } 381 | } 382 | return replayDetails; 383 | } 384 | 385 | std::vector getModSkudefs() { 386 | using namespace Internal; 387 | auto modRoot = concatenateWithModRootFolder({}); 388 | auto allModFolders = findAllMatchingDirectories(concatenatePath(modRoot, wildcardAny)); 389 | auto modDetails = std::vector {}; 390 | for(const auto& mod : allModFolders) { 391 | auto modFolder = concatenatePath(modRoot, mod); 392 | auto skudefs = findAllMatchingFiles(concatenatePath(modFolder, wildcardAny + skudefExtension)); 393 | for(const auto& skudef : skudefs) { 394 | auto modName = skudef.substr(0, skudef.find(L'_')); 395 | auto version = skudef.substr(skudef.find(L'_') + 1); 396 | version.erase(version.find_last_of(L'.')); 397 | modDetails.emplace_back(ModDetails{concatenatePath(modFolder, skudef), std::move(modName), std::move(version)}); 398 | } 399 | } 400 | return modDetails; 401 | } 402 | 403 | std::optional> getModSkudefPathsFromReplay(const ReplayDetails& replay) { 404 | using namespace Internal; 405 | constexpr auto ra3 = std::wstring_view{L"ra3"}; 406 | const auto& modName = replay.modName; 407 | if(insensitiveEqual(std::begin(modName), std::end(modName), std::begin(ra3), std::end(ra3))) { 408 | return std::nullopt; 409 | } 410 | auto skudefFileName = modName + L"_" + replay.modVersion + skudefExtension; 411 | auto modRoot = concatenateWithModRootFolder({}); 412 | auto modFolders = findAllMatchingDirectories(concatenatePath(modRoot, wildcardAny)); 413 | auto mods = std::vector {}; 414 | for(const auto& modFolder : modFolders) { 415 | auto requiredFileName = concatenatePath(concatenatePath(modRoot, modFolder), skudefFileName); 416 | if(fileExists(requiredFileName)) { 417 | mods.emplace_back(std::move(requiredFileName)); 418 | } 419 | } 420 | return mods; 421 | } 422 | } 423 | -------------------------------------------------------------------------------- /WindowsWrapper.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | namespace Windows { 17 | template 18 | struct LocalMemoryDeleter { 19 | using pointer = std::decay_t; 20 | void operator()(pointer memory) const noexcept { 21 | LocalFree(static_cast(memory)); 22 | } 23 | }; 24 | 25 | template 26 | using LocalMemory = std::unique_ptr>; 27 | 28 | inline std::string toBytes(std::wstring_view wideString, UINT codePage = CP_UTF8); 29 | inline std::wstring toWide(std::string_view byteString, UINT codePage = CP_UTF8); 30 | 31 | class WindowsErrorCategory : public std::error_category { 32 | static constexpr char selfName[] = "MyWindowsErrorCategory"; 33 | public: 34 | virtual const char* name() const noexcept { 35 | return selfName; 36 | } 37 | virtual std::string message(int messageID) const { 38 | auto getErrorMessage = [](DWORD messageID, DWORD completeLanguageID) { 39 | auto messageBuffer = LPWSTR{}; 40 | auto size = FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, 41 | nullptr, messageID, completeLanguageID, reinterpret_cast(&messageBuffer), 0, nullptr); 42 | auto message = std::string {""}; 43 | if(size > 0) { 44 | try { 45 | auto buf = LocalMemory {messageBuffer}; 46 | message.assign(toBytes({buf.get(), size})); 47 | } 48 | catch(...) { } 49 | } 50 | return message; 51 | }; 52 | auto message = getErrorMessage(messageID, MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US)); 53 | message += '\n'; 54 | message += getErrorMessage(messageID, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT)); 55 | return message; 56 | } 57 | }; 58 | 59 | inline const WindowsErrorCategory& GetWindowsCategory() { 60 | auto lastError = GetLastError(); 61 | static thread_local auto windowsErrorCategory = WindowsErrorCategory {}; 62 | SetLastError(lastError); 63 | return windowsErrorCategory; 64 | } 65 | 66 | template 67 | struct CheckWin32ResultProxy { 68 | const char* name; 69 | Predicate predicate; 70 | ErrorCodeType lastError; 71 | }; 72 | template 73 | CheckWin32ResultProxy(const char*, Predicate, ErrorCodeType) -> CheckWin32ResultProxy; 74 | 75 | inline constexpr std::equal_to<> successValue; 76 | inline constexpr std::not_equal_to<> errorValue; 77 | inline constexpr struct SuccessIf { } successIf; 78 | inline constexpr struct ResultIsErrorCode { } resultIsErrorCode; 79 | 80 | template 81 | ReturnType operator>>(ReturnType result, const CheckWin32ResultProxy& proxy) { 82 | if(not proxy.predicate(result)) { 83 | if constexpr (std::is_same_v) { 84 | throw std::system_error(result, WindowsErrorCategory(), std::string{proxy.name} + " failed"); 85 | } 86 | else { 87 | throw std::system_error(proxy.lastError, WindowsErrorCategory(), std::string{proxy.name} + " failed"); 88 | } 89 | } 90 | return result; 91 | } 92 | 93 | template 94 | auto checkWin32Result(const char* actionName, ExpectKind resultChecker, const Expected& expected, ErrorCodeType lastError = GetLastError()) { 95 | if constexpr(std::is_same_v) { 96 | return CheckWin32ResultProxy{actionName, expected, lastError}; 97 | } 98 | else { 99 | return CheckWin32ResultProxy{actionName, std::bind(resultChecker, expected, std::placeholders::_1), lastError}; 100 | } 101 | } 102 | 103 | inline std::wstring toWide(std::string_view byteString, UINT codePage /* = CP_UTF8 */) { 104 | if(byteString.empty()) { 105 | return std::wstring {}; 106 | } 107 | 108 | auto neededCharacters = MultiByteToWideChar(codePage, 0, byteString.data(), byteString.size(), nullptr, 0); 109 | neededCharacters >> checkWin32Result("MultiByteToWideChar", errorValue, 0); 110 | 111 | auto buf = std::make_unique(static_cast(neededCharacters)); 112 | 113 | auto convertedCharacters = MultiByteToWideChar(codePage, 0, byteString.data(), byteString.size(), buf.get(), neededCharacters); 114 | convertedCharacters >> checkWin32Result("MultiByteToWideChar", successValue, neededCharacters); 115 | 116 | return std::wstring {buf.get(), static_cast(convertedCharacters)}; 117 | } 118 | 119 | inline std::string toBytes(std::wstring_view wideString, UINT codePage /* = CP_UTF8 */) { 120 | if(wideString.empty()) { 121 | return std::string {}; 122 | } 123 | 124 | auto neededCharacters = WideCharToMultiByte(codePage, 0, wideString.data(), wideString.size(), nullptr, 0, nullptr, nullptr); 125 | neededCharacters >> checkWin32Result("MultiByteToWideChar", errorValue, 0); 126 | 127 | auto buf = std::make_unique(static_cast(neededCharacters)); 128 | 129 | auto convertedCharacters = WideCharToMultiByte(codePage, 0, wideString.data(), wideString.size(), buf.get(), neededCharacters, nullptr, nullptr); 130 | convertedCharacters >> checkWin32Result("MultiByteToWideChar", successValue, neededCharacters); 131 | 132 | return std::string {buf.get(), static_cast(convertedCharacters)}; 133 | } 134 | 135 | template struct NullableStruct { 136 | constexpr NullableStruct(std::nullptr_t = nullptr) noexcept { } 137 | constexpr NullableStruct(std::nullopt_t) noexcept { } 138 | constexpr NullableStruct(const std::optional& data) : data{data} { } 139 | constexpr NullableStruct(std::optional&& data) : data{std::move(data)} { } 140 | constexpr T& operator*() { return this->data.value(); } 141 | constexpr const T& operator*() const { return this->data.value(); } 142 | constexpr std::optional& operator->() { return this->data; } 143 | constexpr const std::optional& operator->() const { return this->data; } 144 | constexpr explicit operator bool() { return static_cast(this->data); } 145 | friend constexpr bool operator==(const NullableStruct& a, const NullableStruct& b) noexcept { return a.data == b.data; } 146 | friend constexpr bool operator!=(const NullableStruct& a, const NullableStruct& b) noexcept { return not (a.data == b.data); } 147 | std::optional data; 148 | }; 149 | 150 | /*struct WindowClassData { 151 | bool operator==(const WindowClassData& b) const noexcept { return this->atom == b.atom and this->module == b.module; } 152 | LPCWSTR atom; 153 | HINSTANCE module; 154 | }; 155 | 156 | struct WindowClassDeleter { 157 | using pointer = NullableStruct; 158 | void operator()(const pointer& windowClass) const noexcept { 159 | try { 160 | UnregisterClassW(windowClass->atom, windowClass->module) 161 | >> checkWin32Result("UnregisterClassW", errorValue, FALSE); 162 | } 163 | catch(const std::exception& e) { 164 | MessageBoxA(nullptr, e.what(), 0, MB_OK | MB_ICONERROR); 165 | } 166 | catch(...) { 167 | MessageBoxA(nullptr, "UnregisterClassW failed", 0, MB_OK | MB_ICONERROR); 168 | } 169 | } 170 | }; 171 | 172 | using WindowClassHolder = std::unique_ptr; 173 | 174 | template 175 | WindowClassHolder registerWindowClass(const std::wstring& className, HICON icon, 176 | HINSTANCE moduleInstance = GetModuleHandle(nullptr)) { 177 | auto windowClassExtended = WNDCLASSEXW {}; 178 | windowClassExtended.cbSize = sizeof(WNDCLASSEX); 179 | windowClassExtended.style = CS_HREDRAW | CS_VREDRAW; 180 | windowClassExtended.lpfnWndProc = WindowType::windowCallbacks; 181 | windowClassExtended.cbClsExtra = 0; 182 | windowClassExtended.cbWndExtra = sizeof(WindowType*); 183 | windowClassExtended.hInstance = moduleInstance; 184 | windowClassExtended.hIcon = icon; 185 | windowClassExtended.hCursor = LoadCursorW(NULL, reinterpret_cast(IDC_ARROW)); 186 | windowClassExtended.hbrBackground = reinterpret_cast(COLOR_WINDOW + 1); 187 | windowClassExtended.lpszMenuName = nullptr; 188 | windowClassExtended.lpszClassName = className.c_str(); 189 | windowClassExtended.hIconSm = icon; 190 | 191 | auto result = ULONG_PTR{RegisterClassExW(&windowClassExtended)} 192 | >> checkWin32Result("RegisterClassExW", errorValue, ULONG_PTR{0}); 193 | return WindowClassHolder{{WindowClassData{reinterpret_cast(result), moduleInstance}}}; 194 | }*/ 195 | 196 | struct WindowDestroyer { 197 | using pointer = HWND; 198 | void operator()(pointer window) const noexcept { 199 | DestroyWindow(window); 200 | } 201 | }; 202 | 203 | using WindowHandle = std::unique_ptr; 204 | 205 | inline WindowHandle createChildWindow(HWND parent, LPCWSTR windowClass, 206 | const std::wstring& windowName, DWORD style, DWORD extendedStyle, 207 | int x, int y, int width, int height, 208 | HMENU menuHandle = nullptr, HINSTANCE moduleInstance = nullptr, LPVOID parameters = nullptr) { 209 | auto rawHandle = CreateWindowExW(extendedStyle, windowClass, windowName.c_str(), style, 210 | x, y, width, height, parent, menuHandle, moduleInstance, parameters); 211 | rawHandle >> checkWin32Result("CreateWindowExW", errorValue, nullptr); 212 | return WindowHandle { rawHandle }; 213 | } 214 | 215 | inline WindowHandle createControl(HWND parent, int childIdentifier, LPCWSTR windowClass, 216 | const std::wstring& windowName, DWORD style, DWORD extendedStyle, 217 | int x, int y, int width, int height, 218 | HINSTANCE moduleInstance = nullptr, LPVOID parameters = nullptr) { 219 | auto rawHandle = CreateWindowExW(extendedStyle, windowClass, windowName.c_str(), style | WS_CHILD, 220 | x, y, width, height, parent, reinterpret_cast(childIdentifier), moduleInstance, parameters) 221 | >> checkWin32Result("CreateWindowExW", errorValue, nullptr); 222 | return WindowHandle { rawHandle }; 223 | } 224 | 225 | inline HWND getControlByID(HWND parent, int childIdentifier) { 226 | return GetDlgItem(parent, childIdentifier) >> checkWin32Result("GetDlgItem", errorValue, nullptr); 227 | } 228 | 229 | inline int getControlID(HWND childWindow) { 230 | return GetDlgCtrlID(childWindow) >> checkWin32Result("GetDlgCtrlID", errorValue, 0); 231 | } 232 | 233 | struct ModalDialogBox { 234 | using MessageHandler = std::function; 235 | using HandlerTable = std::unordered_map; 236 | 237 | static INT_PTR CALLBACK dialogCallbacks(HWND windowHandle, UINT messageID, WPARAM wParam, LPARAM lParam) noexcept { 238 | if(messageID == WM_INITDIALOG) { 239 | SetLastError(ERROR_SUCCESS); 240 | //If messageID is WM_INITDIALOG, then set long ptr 241 | SetWindowLongPtrW(windowHandle, DWLP_USER, lParam); 242 | auto lastError = GetLastError(); 243 | try { 244 | lastError >> checkWin32Result("SetWindowLongPtrW", successValue, DWORD{ERROR_SUCCESS}); 245 | } 246 | catch(...) { 247 | //If there errors occurred when seeting DWLP_USER... 248 | reinterpret_cast(lParam)->exceptionLocation = std::current_exception(); 249 | EndDialog(windowHandle, -1); 250 | } 251 | } 252 | 253 | auto dataAddress = reinterpret_cast(GetWindowLongPtrW(windowHandle, DWLP_USER)); 254 | if(dataAddress == nullptr) { 255 | //If there is no dwlp_user, then return 256 | return FALSE; 257 | } 258 | 259 | auto& data = *dataAddress; 260 | 261 | if((data.callbacksReference.get().count(messageID) == 0) or (data.exceptionLocation != nullptr)) { 262 | //If there is no callbacks, then return 263 | return FALSE; 264 | } 265 | 266 | try { 267 | return data.callbacksReference.get().at(messageID)(windowHandle, wParam, lParam); 268 | } 269 | catch(...) { 270 | data.exceptionLocation = std::current_exception(); 271 | EndDialog(windowHandle, -1); 272 | return FALSE; 273 | } 274 | } 275 | 276 | std::reference_wrapper callbacksReference; 277 | std::exception_ptr exceptionLocation; 278 | }; 279 | 280 | inline INT_PTR modalDialogBox(const ModalDialogBox::HandlerTable& callbacks, DWORD styles, DWORD extendedStyles, HWND parent = nullptr, HINSTANCE moduleHandle = nullptr) { 281 | auto buffer = std::aligned_storage_t {}; 282 | auto& dialogBoxTemplate = *reinterpret_cast(&buffer); 283 | dialogBoxTemplate = DLGTEMPLATE{styles, extendedStyles}; 284 | auto data = ModalDialogBox{callbacks}; 285 | auto returnValue = DialogBoxIndirectParam(moduleHandle, &dialogBoxTemplate, parent, &ModalDialogBox::dialogCallbacks, reinterpret_cast(&data)); 286 | if(data.exceptionLocation) { 287 | std::rethrow_exception(data.exceptionLocation); 288 | } 289 | returnValue >> checkWin32Result("DialogBoxIndirectParam", errorValue, -1); 290 | return returnValue; 291 | } 292 | 293 | inline constexpr LONG rectWidth(const RECT& rect) { 294 | return rect.right - rect.left; 295 | } 296 | 297 | inline constexpr LONG rectHeight(const RECT& rect) { 298 | return rect.bottom - rect.top; 299 | } 300 | 301 | inline RECT getClientRect(HWND windowHandle) { 302 | auto clientRect = RECT{}; 303 | GetClientRect(windowHandle, &clientRect) >> checkWin32Result("GetClientRect", errorValue, false); 304 | return clientRect; 305 | } 306 | 307 | inline RECT getWindowRect(HWND windowHandle) { 308 | auto windowRect = RECT{}; 309 | GetWindowRect(windowHandle, &windowRect) >> checkWin32Result("GetClientRect", errorValue, false); 310 | return windowRect; 311 | } 312 | 313 | inline RECT setWindowRectByClientRect(HWND windowHandle, int width, int height) { 314 | auto expectedClientRect = RECT {0, 0, width, height}; 315 | auto realClientRect = getClientRect(windowHandle); 316 | auto windowRect = getWindowRect(windowHandle); 317 | windowRect.left += expectedClientRect.left - realClientRect.left; 318 | windowRect.top += expectedClientRect.top - realClientRect.top; 319 | windowRect.right += expectedClientRect.right - realClientRect.right; 320 | windowRect.bottom += expectedClientRect.bottom - realClientRect.bottom; 321 | return windowRect; 322 | } 323 | 324 | struct DeviceContextDeleter { 325 | using pointer = HDC; 326 | void operator()(pointer deviceContext) const noexcept { 327 | DeleteDC(deviceContext) >> checkWin32Result("DeleteDC", errorValue, false); 328 | } 329 | }; 330 | 331 | using DeviceContextOwner = std::unique_ptr; 332 | 333 | inline DeviceContextOwner createCompatibleDeviceContext(HDC existingContext) { 334 | return DeviceContextOwner{CreateCompatibleDC(existingContext) 335 | >> checkWin32Result("CreateCompatibleDC", errorValue, nullptr)}; 336 | } 337 | 338 | struct WindowDeviceContext { 339 | bool operator==(const WindowDeviceContext& b) const noexcept { return this->context == b.context and this->window == b.window; } 340 | HWND window; 341 | HDC context; 342 | }; 343 | 344 | struct WindowDeviceContextReleaser { 345 | using pointer = NullableStruct; 346 | void operator()(pointer deviceContext) const noexcept { 347 | ReleaseDC(deviceContext->window, deviceContext->context); 348 | } 349 | }; 350 | 351 | using RetrievedWindowDeviceContext = std::unique_ptr; 352 | 353 | inline RetrievedWindowDeviceContext getDeviceContext(HWND windowHandle) { 354 | return RetrievedWindowDeviceContext{{ 355 | WindowDeviceContext{ 356 | windowHandle, 357 | GetDC(windowHandle) >> checkWin32Result("GetDC", errorValue, nullptr) 358 | } 359 | }}; 360 | } 361 | 362 | struct PaintStruct { 363 | bool operator==(const PaintStruct& b) const noexcept { return this->window == b.window and this->context == b.context; } 364 | HWND window; 365 | PAINTSTRUCT paintStruct; 366 | HDC context; 367 | }; 368 | 369 | struct PaintStructDeleter { 370 | using pointer = NullableStruct; 371 | void operator()(const pointer& paintStruct) const noexcept { 372 | EndPaint(paintStruct->window, &(paintStruct->paintStruct)); 373 | } 374 | }; 375 | 376 | using PaintStructHolder = std::unique_ptr; 377 | 378 | inline PaintStructHolder beginPaint(HWND windowHandle) { 379 | auto paintStruct = PAINTSTRUCT{}; 380 | auto context = BeginPaint(windowHandle, &paintStruct) 381 | >> checkWin32Result("BeginPaint", errorValue, nullptr); 382 | return PaintStructHolder{{PaintStruct{windowHandle, paintStruct, context}}}; 383 | } 384 | 385 | template 386 | struct GDIPreviousObject { 387 | bool operator==(const GDIPreviousObject& b) const noexcept { return this->context == b.context and this->object == b.object; } 388 | HDC context; 389 | T object; 390 | }; 391 | template 392 | GDIPreviousObject(HDC, T) -> GDIPreviousObject; 393 | 394 | template 395 | struct GDISelectObjectRestorer { 396 | using pointer = NullableStruct>; 397 | void operator()(const pointer& previous) const noexcept { 398 | try { 399 | restore(previous->context, previous->object).release(); 400 | } 401 | catch(const std::exception& e) { 402 | MessageBoxW(nullptr, toWide(e.what()).c_str(), 0, MB_OK | MB_ICONERROR); 403 | } 404 | } 405 | SelectObjectFunctor restore; 406 | }; 407 | 408 | 409 | template 410 | using GDISelectObjectRestoreData = std::unique_ptr; 411 | 412 | struct SelectObjectFunctor { 413 | auto operator()(HDC context, HGDIOBJ newObject) const { 414 | auto previous = SelectObject(context, newObject) 415 | >> checkWin32Result("SelectObject", successIf, [](HGDIOBJ result) { 416 | return (result != nullptr) and (result != HGDI_ERROR); 417 | }); 418 | using Restorer = GDISelectObjectRestorer; 419 | return GDISelectObjectRestoreData {{GDIPreviousObject{context, previous}}, Restorer{*this}}; 420 | } 421 | }; 422 | inline constexpr SelectObjectFunctor selectObject; 423 | 424 | struct SelectBackgroundModeFunctor { 425 | auto operator()(HDC context, int newBackgroundMode) const { 426 | auto previous = SetBkMode(context, newBackgroundMode) >> checkWin32Result("SetBkMode", errorValue, 0); 427 | using Restorer = GDISelectObjectRestorer; 428 | return GDISelectObjectRestoreData {{GDIPreviousObject{context, previous}}, Restorer{*this}}; 429 | } 430 | }; 431 | inline constexpr SelectBackgroundModeFunctor setBackgroundMode; 432 | 433 | struct SelectTextColorFunctor { 434 | auto operator()(HDC context, COLORREF newTextColor) const { 435 | auto previous = SetTextColor(context, newTextColor) >> checkWin32Result("SetTextColor", errorValue, CLR_INVALID); 436 | using Restorer = GDISelectObjectRestorer; 437 | return GDISelectObjectRestoreData {{GDIPreviousObject{context, previous}}, Restorer{*this}}; 438 | } 439 | }; 440 | inline constexpr SelectTextColorFunctor setTextColor; 441 | 442 | template 443 | struct TaggedHandle { 444 | using Handle = typename std::unique_ptr::pointer; 445 | static constexpr auto typeID = tag; 446 | constexpr TaggedHandle() noexcept = default; 447 | explicit TaggedHandle(Handle handle) noexcept : handle { handle } { } 448 | TaggedHandle(TaggedHandle&& other) noexcept : handle { std::move(other.handle) } { } 449 | template TaggedHandle(Args&&... args) noexcept : handle { std::forward(args)... } { } 450 | TaggedHandle& operator=(TaggedHandle&& other) noexcept { this->handle = std::move(other.handle); return *this; }; 451 | Handle get() const noexcept { return this->handle.get(); } 452 | private: 453 | std::unique_ptr handle; 454 | }; 455 | 456 | template 457 | struct GDIObjectDeleter { 458 | using pointer = HandleType; 459 | void operator()(pointer objectHandle) const noexcept { 460 | DeleteObject(objectHandle); 461 | } 462 | }; 463 | 464 | struct IconDeleter { 465 | using pointer = HICON; 466 | void operator()(pointer bitmap) const noexcept { 467 | DestroyIcon(bitmap); 468 | } 469 | }; 470 | 471 | using BitmapHandle = TaggedHandle>; 472 | using BrushHandle = std::unique_ptr>; 473 | using FontHandle = std::unique_ptr>; 474 | using IconHandle = TaggedHandle; 475 | 476 | inline BitmapHandle createCompatibleBitmap(HDC deviceContext, int width, int height) { 477 | return BitmapHandle{CreateCompatibleBitmap(deviceContext, width, height) 478 | >> checkWin32Result("CreateComspatibleBitmap", errorValue, nullptr)}; 479 | } 480 | 481 | template 482 | ImageHandle loadImage(const std::wstring& fileName, std::pair xy = {0, 0}) { 483 | auto result = LoadImageW(nullptr, fileName.c_str(), ImageHandle::typeID, xy.first, xy.second, LR_LOADFROMFILE|LR_DEFAULTSIZE) 484 | >> checkWin32Result("LoadImageW", errorValue, nullptr); 485 | return ImageHandle { reinterpret_cast(result) }; 486 | } 487 | 488 | template 489 | ImageHandle loadImage(HINSTANCE module, WORD resourceID, std::pair xy = {0, 0}) { 490 | auto result = LoadImageW(module, MAKEINTRESOURCEW(resourceID), ImageHandle::typeID, xy.first, xy.second, LR_DEFAULTSIZE) 491 | >> checkWin32Result("LoadImageW", errorValue, nullptr); 492 | return ImageHandle { reinterpret_cast(result) }; 493 | } 494 | 495 | inline BrushHandle createPatternBrush(HBITMAP bitmap) { 496 | return BrushHandle { CreatePatternBrush(bitmap) 497 | >> checkWin32Result("CreatePatternBrush", errorValue, nullptr) }; 498 | } 499 | 500 | inline FontHandle createFontIndirect(const LOGFONTW& fontAttributes) { 501 | return FontHandle { CreateFontIndirectW(&fontAttributes) 502 | >> checkWin32Result("CreateFontIndirect", errorValue, nullptr) }; 503 | } 504 | 505 | struct BitmapBits { 506 | std::pair imageSize() const noexcept { 507 | return {this->bitmapInfo.bmiHeader.biWidth, std::abs(this->bitmapInfo.bmiHeader.biHeight)}; 508 | } 509 | BITMAPINFO bitmapInfo; 510 | std::vector buffer; 511 | }; 512 | 513 | inline BitmapBits getBitmapBits(HDC deviceContext, HBITMAP bitmapHandle, std::optional> requiredWidthHeight = std::nullopt) { 514 | auto bitmapInfo = BITMAPINFO{}; 515 | auto& header = bitmapInfo.bmiHeader; 516 | header.biSize = sizeof(BITMAPINFOHEADER); 517 | 518 | GetDIBits(deviceContext, bitmapHandle, 0, /*requiredAbsoluteHeight*/0, nullptr, &bitmapInfo, DIB_RGB_COLORS) 519 | >> checkWin32Result("GetDIBIts (retrieving bitmapinfo)", errorValue, false); 520 | 521 | if(header.biBitCount != 32) { 522 | throw std::runtime_error("Cannot get 32 bit pixels, got " + std::to_string(header.biBitCount) + " instead."); 523 | } 524 | 525 | auto absoluteHeight = std::abs(header.biHeight); 526 | if(requiredWidthHeight.has_value()) { 527 | auto [requiredWidth, requiredHeight] = requiredWidthHeight.value(); 528 | if(requiredWidth != header.biWidth) { 529 | throw std::runtime_error("Image width != required width"); 530 | } 531 | 532 | auto requiredAbsoluteHeight = std::abs(requiredHeight); 533 | 534 | if(absoluteHeight != requiredAbsoluteHeight) { 535 | throw std::runtime_error("Image height != required height"); 536 | } 537 | header.biHeight = requiredHeight; 538 | } 539 | 540 | header.biCompression = BI_RGB; 541 | auto buffer = std::vector {}; 542 | buffer.resize(header.biWidth * absoluteHeight); 543 | GetDIBits(deviceContext, bitmapHandle, 0, absoluteHeight, buffer.data(), &bitmapInfo, DIB_RGB_COLORS) 544 | >> checkWin32Result("GetDIBIts", errorValue, false); 545 | 546 | return {bitmapInfo, std::move(buffer)}; 547 | } 548 | 549 | inline void setBitmapBits(HDC deviceContext, HBITMAP bitmapHandle, const BitmapBits& bits) { 550 | SetDIBits(deviceContext, bitmapHandle, 0, bits.imageSize().second, 551 | bits.buffer.data(), &(bits.bitmapInfo), DIB_RGB_COLORS) 552 | >> checkWin32Result("SetDIBIts", errorValue, false); 553 | } 554 | 555 | template 556 | std::vector loadBinaryDataResource(HMODULE module, LPCWSTR resource, LPCWSTR type) { 557 | auto resourceInformation = FindResourceW(module, resource, type) 558 | >> checkWin32Result("FindResourceW", errorValue, nullptr); 559 | auto resourceHandle = LoadResource(module, resourceInformation) 560 | >> checkWin32Result("LoadResource", errorValue, nullptr); 561 | auto data = static_cast(LockResource(resourceHandle)) 562 | >> checkWin32Result("LockResource", errorValue, nullptr); 563 | auto bytes = SizeofResource(module, resourceInformation) 564 | >> checkWin32Result("SizeofResource", errorValue, 0); 565 | 566 | auto buffer = std::vector {}; 567 | auto count = bytes / sizeof(ValueType); 568 | buffer.resize(count); 569 | std::copy_n(data, count, std::begin(buffer)); 570 | return buffer; 571 | } 572 | 573 | struct HandleDeleter { 574 | using pointer = HANDLE; 575 | void operator()(pointer handle) const noexcept { 576 | CloseHandle(handle); 577 | } 578 | }; 579 | 580 | using Handle = std::unique_ptr; 581 | 582 | inline Handle createFile(const std::wstring& fileName, DWORD desiredAccess, DWORD shareMode, DWORD creationDisposition, 583 | DWORD flagAndAttributes = FILE_ATTRIBUTE_NORMAL, LPSECURITY_ATTRIBUTES securityAttributes = nullptr, HANDLE fileTemplate = nullptr) { 584 | return Handle { CreateFileW(fileName.c_str(), desiredAccess, shareMode, securityAttributes, 585 | creationDisposition, flagAndAttributes, fileTemplate) 586 | >> checkWin32Result(("CreateFileW [" + toBytes(fileName) + ']').c_str(), errorValue, INVALID_HANDLE_VALUE) }; 587 | } 588 | 589 | inline std::size_t getFileSize(HANDLE fileHandle) { 590 | auto fileSize = LARGE_INTEGER{}; 591 | GetFileSizeEx(fileHandle, &fileSize) >> checkWin32Result("GetFileSizeEx", errorValue, false); 592 | if(fileSize.QuadPart < 0) { 593 | throw std::runtime_error("File size is negative"); 594 | } 595 | return static_cast(fileSize.QuadPart); 596 | } 597 | 598 | inline LONGLONG setFilePointer(HANDLE fileHandle, LONGLONG distanceToMove, DWORD moveMethod) { 599 | auto distanceAsLargeInteger = LARGE_INTEGER{}; 600 | distanceAsLargeInteger.QuadPart = distanceToMove; 601 | auto result = LARGE_INTEGER{}; 602 | SetFilePointerEx(fileHandle, distanceAsLargeInteger, &result, moveMethod) >> checkWin32Result("SetFilePointerEx", errorValue, false); 603 | return result.QuadPart; 604 | } 605 | 606 | inline bool isDirectory(const std::wstring& path) { 607 | auto attributes = GetFileAttributesW(path.c_str()); 608 | return (attributes != INVALID_FILE_ATTRIBUTES) and (attributes bitand FILE_ATTRIBUTE_DIRECTORY); 609 | } 610 | 611 | inline bool fileExists(const std::wstring& fileName) { 612 | auto attributes = GetFileAttributesW(fileName.c_str()); 613 | return (attributes != INVALID_FILE_ATTRIBUTES) and not (attributes bitand FILE_ATTRIBUTE_DIRECTORY); 614 | } 615 | 616 | struct FindCloser { 617 | using pointer = HANDLE; 618 | void operator()(pointer handle) const noexcept { 619 | FindClose(handle); 620 | } 621 | }; 622 | 623 | using FindHandle = std::unique_ptr; 624 | 625 | template 626 | inline std::vector findAllMatching(const std::wstring& path, Predicate predicate) { 627 | auto data = WIN32_FIND_DATAW{}; 628 | auto rawHandle = FindFirstFileW(path.c_str(), &data); 629 | auto lastError = GetLastError(); 630 | if(rawHandle == INVALID_HANDLE_VALUE and lastError == ERROR_FILE_NOT_FOUND) { 631 | return {}; 632 | } 633 | auto handle = FindHandle {rawHandle >> checkWin32Result(("FindFirstFile [" + toBytes(path) + ']').c_str(), errorValue, INVALID_HANDLE_VALUE, lastError)}; 634 | 635 | auto fileNames = std::vector {}; 636 | auto nextFileExist = false; 637 | do { 638 | if(predicate(data)) { 639 | fileNames.emplace_back(data.cFileName); 640 | } 641 | 642 | nextFileExist = FindNextFileW(handle.get(), &data); 643 | lastError = GetLastError(); 644 | if(lastError != ERROR_NO_MORE_FILES) { 645 | nextFileExist >> checkWin32Result("FindNextFile", errorValue, false, lastError); 646 | } 647 | } 648 | while(nextFileExist); 649 | return fileNames; 650 | } 651 | 652 | inline std::vector findAllMatchingFiles(const std::wstring& path) { 653 | return findAllMatching(path, [](const WIN32_FIND_DATAW& data) { return data.dwFileAttributes xor FILE_ATTRIBUTE_DIRECTORY; }); 654 | } 655 | 656 | inline std::vector findAllMatchingDirectories(const std::wstring& path) { 657 | return findAllMatching(path, [](const WIN32_FIND_DATAW& data) { return data.dwFileAttributes bitand FILE_ATTRIBUTE_DIRECTORY; }); 658 | } 659 | 660 | inline void appendToFolder(std::wstring& folder, std::wstring_view subPath) { 661 | if(not folder.empty() and PathGetCharType(folder.back()) != GCT_SEPARATOR) { 662 | folder += L'\\'; 663 | } 664 | folder += subPath; 665 | } 666 | 667 | inline std::wstring concatenatePath(std::wstring folder, std::wstring_view subPath) { 668 | appendToFolder(folder, subPath); 669 | return folder; 670 | } 671 | 672 | inline void shellExecute(HWND associatedWindow, LPCWSTR operation, const std::wstring& file, int showMode = SW_SHOWNORMAL, 673 | LPCWSTR parameters = nullptr, LPCWSTR directory = nullptr) { 674 | 675 | auto info = SHELLEXECUTEINFOW{sizeof(SHELLEXECUTEINFOW)}; 676 | info.fMask = SEE_MASK_DEFAULT; 677 | info.hwnd = associatedWindow; 678 | info.lpVerb = operation; 679 | info.lpFile = file.c_str(); 680 | info.lpParameters = parameters; 681 | info.lpDirectory = directory; 682 | info.nShow = showMode; 683 | ShellExecuteExW(&info) >> checkWin32Result("ShellExecuteExW", errorValue, false); 684 | } 685 | 686 | inline Handle createProcess(std::wstring commandLine, LPCWSTR currentDirectory, 687 | DWORD creationFlags = 0, bool inheritHandles = false, LPVOID environments = nullptr, LPSTARTUPINFOW startUpInfo = nullptr, 688 | LPSECURITY_ATTRIBUTES processAttributes = nullptr, LPSECURITY_ATTRIBUTES threadAttribtues = nullptr) { 689 | auto defaultStartUpInfo = STARTUPINFOW{sizeof(STARTUPINFOW)}; 690 | if(startUpInfo == nullptr) { 691 | startUpInfo = &defaultStartUpInfo; 692 | } 693 | auto processInformation = PROCESS_INFORMATION{}; 694 | CreateProcessW(nullptr, commandLine.data(), processAttributes, threadAttribtues, 695 | inheritHandles, creationFlags, environments, currentDirectory, startUpInfo, &processInformation) 696 | >> checkWin32Result("CreateProcessW", errorValue, false); 697 | Handle{processInformation.hThread}; 698 | return Handle{processInformation.hProcess}; 699 | } 700 | 701 | struct RegistryKeyCloser { 702 | using pointer = HKEY; 703 | void operator()(pointer keyHandle) const noexcept { 704 | RegCloseKey(keyHandle); 705 | } 706 | }; 707 | 708 | using RegistryKey = std::unique_ptr; 709 | 710 | inline RegistryKey openRegistryKey(HKEY root, const std::wstring& path, REGSAM securityAccessMask = KEY_READ) { 711 | auto rawHandle = HKEY{nullptr}; 712 | RegOpenKeyExW(root, path.c_str(), 0, securityAccessMask, &rawHandle) 713 | >> checkWin32Result("RegOpenKeyExW", successValue, ERROR_SUCCESS, resultIsErrorCode); 714 | return RegistryKey{rawHandle}; 715 | } 716 | 717 | inline std::wstring getRegistryString(HKEY registryKey, const std::wstring& valueName) { 718 | auto valueType = DWORD{}; 719 | auto bufferBytes = DWORD{}; 720 | RegQueryValueExW(registryKey, valueName.c_str(), nullptr, &valueType, nullptr, &bufferBytes) 721 | >> checkWin32Result("RegGetValue (retrieving buffer size)", successValue, ERROR_SUCCESS, resultIsErrorCode); 722 | 723 | if(valueType != REG_SZ) { 724 | throw std::invalid_argument("getRegistryString() failed: value type may not be string"); 725 | } 726 | 727 | if(bufferBytes % sizeof(wchar_t) != 0) { 728 | throw std::invalid_argument("getRegistryString() failed: value type may not be string, bufferBytes % sizeof(wchar_t) != 0"); 729 | } 730 | 731 | auto result = std::wstring{bufferBytes / sizeof(wchar_t), {}, std::wstring::allocator_type{}}; 732 | 733 | RegQueryValueExW(registryKey, valueName.c_str(), nullptr, &valueType, reinterpret_cast(result.data()), &bufferBytes) 734 | >> checkWin32Result("RegGetValue", successValue, ERROR_SUCCESS, resultIsErrorCode); 735 | 736 | if(valueType != REG_SZ) { 737 | throw std::invalid_argument("getRegistryString() failed: value type may not be string"); 738 | } 739 | 740 | if(bufferBytes != (result.size() * sizeof(wchar_t))) { 741 | throw std::runtime_error("getRegistryString() failed, bufferBytes != result.size() * sizeof(wchar_t)"); 742 | } 743 | 744 | constexpr auto nulLength = 1; 745 | if(result.size() >= nulLength and result.back() == '\0') { 746 | result.pop_back(); 747 | } 748 | 749 | return result; 750 | } 751 | 752 | inline void setRegistryString(HKEY registryKey, const std::wstring& valueName, const std::wstring& value) { 753 | RegSetValueExW(registryKey, valueName.c_str(), 0, REG_SZ, 754 | reinterpret_cast(value.c_str()), value.size() * sizeof(wchar_t)) 755 | >> checkWin32Result("RegSetValueEx", successValue, ERROR_SUCCESS, resultIsErrorCode); 756 | } 757 | 758 | template 759 | FILETIME unixTimeToFileTime(T unixTimeStamp) { 760 | auto windowsTimeStamp = static_cast(unixTimeStamp) * 10000000 + 116444736000000000; 761 | auto fileTime = FILETIME{}; 762 | fileTime.dwLowDateTime = static_cast(windowsTimeStamp); 763 | fileTime.dwHighDateTime = windowsTimeStamp >> 32; 764 | return fileTime; 765 | } 766 | } 767 | 768 | -------------------------------------------------------------------------------- /UserInterface.cpp: -------------------------------------------------------------------------------- 1 | //Everything related to user interface is inside this file... 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include "UserInterface.hpp" 17 | #include "WindowsWrapper.hpp" 18 | #include "ReplaysAndMods.hpp" 19 | #include "resource.h" 20 | 21 | using namespace Windows; 22 | using namespace MyCSF; 23 | 24 | enum ID { 25 | errorMessage = 1, 26 | 27 | captionString, 28 | 29 | splashScreen, 30 | //buttons 31 | playGame, 32 | checkForUpdates, 33 | setLanguage, 34 | gameBrowser, 35 | readMe, 36 | visitEAWebsite, 37 | technicalSupport, 38 | deauthorize, 39 | quit, 40 | about, 41 | // 42 | commandLine, 43 | //set language window 44 | setLanguageDescription, 45 | setLanguageList, 46 | setLanguageOK, 47 | setLanguageCancel, 48 | //game browser window 49 | gameBrowserTabs, 50 | gameBrowserLaunchGame, 51 | gameBrowserCancel, 52 | //game browser mod window 53 | mods, 54 | modList, 55 | modListModName, 56 | modListModVersion, 57 | modFolder, 58 | //game browser replay window 59 | replays, 60 | replayList, 61 | replayListReplayName, 62 | replayListModName, 63 | replayListGameVersion, 64 | replayListDate, 65 | getReplayDescriptionBox, 66 | replayMatchInformation, 67 | replayMap, 68 | replayNumberOfPlayers, 69 | replayDescription, 70 | fixReplay, 71 | fixReplayWarning, 72 | fixReplaySucceeded, 73 | fixReplayFailed, 74 | replayFolder, 75 | //launcher / game browser launching game (replay) 76 | replayCantBePlayed, 77 | replayCantBeParsedText, 78 | replayDontHaveCommentator, 79 | replayNeedsToBeFixed, 80 | replayNeedsToBeFixedText, 81 | 82 | webSiteLink, 83 | eaSupportURL, 84 | 85 | useOriginalRA3Title, 86 | useOriginalRA3ToUpdate, 87 | useOriginalRA3ToDeauthorize, 88 | 89 | aboutText, 90 | resourceAuthors, 91 | 92 | gameVersionNotFound, 93 | noModsFound, 94 | multipleModsFound, 95 | }; 96 | 97 | const std::wstring& getText(const LanguageData& data, ID id) { 98 | static auto labels = std::map { 99 | {errorMessage, L"RA3BarLauncher:ErrorMessage"}, 100 | {captionString, L"Launcher:Caption"}, 101 | {splashScreen, L"RA3BarLauncher:ClickSplashScreen"}, 102 | {playGame, L"Launcher:Play"}, 103 | {checkForUpdates, L"Launcher:CheckForUpdates"}, 104 | {setLanguage, L"Launcher:SelectLanguage"}, 105 | {gameBrowser, L"Launcher:ReplayBrowser"}, 106 | {readMe, L"Launcher:Readme"}, 107 | {visitEAWebsite, L"Launcher:Website"}, 108 | {technicalSupport, L"Launcher:TechnicalSupport"}, 109 | {deauthorize, L"Launcher:DeAuthorize"}, 110 | {quit, L"Launcher:Quit"}, 111 | {about, L"RA3BarLauncher:About"}, 112 | {commandLine, L"RA3BarLauncher:CommandLine"}, 113 | {setLanguageDescription, L"Launcher:SelectLanguageText"}, 114 | {setLanguageOK, L"Dialog:OK"}, 115 | {setLanguageCancel, L"Dialog:Cancel"}, 116 | {gameBrowserLaunchGame, L"REPLAYBROWSER:WATCHREPLAY"}, 117 | {gameBrowserCancel, L"Dialog:Cancel"}, 118 | {mods, L"LAUNCHER:MODTAB"}, 119 | {modListModName, L"MODBROWSER:NAMECOLUMN"}, 120 | {modListModVersion, L"MODBROWSER:VERSIONCOLUMN"}, 121 | {modFolder, L"RA3BarLauncher:OpenModFolder"}, 122 | {replays, L"LAUNCHER:REPLAYTAB"}, 123 | {replayListReplayName, L"REPLAYBROWSER:NAMECOLUMN"}, 124 | {replayListModName, L"REPLAYBROWSER:MODCOLUMN"}, 125 | {replayListGameVersion, L"REPLAYBROWSER:VERSIONCOLUMN"}, 126 | {replayListDate, L"REPLAYBROWSER:DATECOLUMN"}, 127 | {replayMatchInformation, L"REPLAYBROWSER:MATCHINFO"}, 128 | {replayMap, L"REPLAYBROWSER:MAP"}, 129 | {replayNumberOfPlayers, L"REPLAYBROWSER:NUMPLAYERS"}, 130 | {replayDescription, L"REPLAYBROWSER:DESCRIPTION"}, 131 | {fixReplay, L"RA3BarLauncher:FixReplay"}, 132 | {fixReplayWarning, L"RA3BarLauncher:FixReplayWillReplaceOriginal"}, 133 | {fixReplaySucceeded, L"RA3BarLauncher:FixReplaySuccess"}, 134 | {fixReplayFailed, L"RA3BarLauncher:FixReplayFailure"}, 135 | {replayFolder, L"RA3BarLauncher:OpenReplayFolder"}, 136 | {webSiteLink, L"Launcher:URL"}, 137 | {eaSupportURL, L"RA3BarLauncher:EASupportWebsite"}, 138 | {useOriginalRA3Title, L"RA3BarLauncher:NeedOriginalLauncher"}, 139 | {useOriginalRA3ToUpdate, L"RA3BarLauncher:UpdateNotSupported"}, 140 | {useOriginalRA3ToDeauthorize, L"RA3BarLauncher:DeauthorizeNotSupported"}, 141 | {replayCantBePlayed, L"RA3BarLauncher:ReplayCannotBePlayed"}, 142 | {replayCantBeParsedText, L"RA3BarLauncher:ReplayCannotBeParsed"}, 143 | {replayDontHaveCommentator, L"RA3BarLauncher:ReplayDoesNotHaveCommentator"}, 144 | {replayNeedsToBeFixed, L"RA3BarLauncher:ReplayNeedsToBeFixed"}, 145 | {replayNeedsToBeFixedText, L"RA3BarLauncher:ReplayNeedsToBeFixedText"}, 146 | {aboutText, L"RA3BarLauncher:AboutText"}, 147 | {resourceAuthors, L"RA3BarLauncher:ResourceAuthors"}, 148 | {gameVersionNotFound, L"Launcher:CantFindVersionN_InstallPatches"}, 149 | {noModsFound, L"REPLAYBROWSER:MODNOTINSTALLED"}, 150 | {multipleModsFound, L"RA3BarLauncher:ReplayModAmbiguity"}, 151 | }; 152 | 153 | auto loadMyCSF = [](HMODULE module, LPCWSTR resource, LPCWSTR type) { 154 | auto data = loadBinaryDataResource(module, resource, type); 155 | return loadCSFStrings(std::begin(data), std::end(data)); 156 | }; 157 | static auto myEnglish = loadMyCSF(GetModuleHandle(nullptr), MAKEINTRESOURCEW(BUILTIN_ENGLISH), RT_RCDATA); 158 | static auto myChinese = loadMyCSF(GetModuleHandle(nullptr), MAKEINTRESOURCEW(BUILTIN_CHINESE), RT_RCDATA); 159 | 160 | auto currentMyMap = &myEnglish; 161 | auto chinese = std::wstring_view{L"chinese"}; 162 | if(insensitiveSearch(std::begin(data.languageName), std::end(data.languageName), 163 | std::begin(chinese), std::end(chinese)) != std::end(data.languageName)) { 164 | currentMyMap = &myChinese; 165 | } 166 | 167 | const auto& label = labels.at(id); 168 | 169 | auto string = data.table.find(label); 170 | if(string == data.table.end()) { 171 | string = currentMyMap->find(label); 172 | if(string == currentMyMap->end()) { 173 | static auto error = std::wstring{L""}; 174 | return error; 175 | } 176 | } 177 | return string->second; 178 | } 179 | 180 | const std::wstring& getLanguageName(const LanguageData& data, const std::wstring& languageName) { 181 | 182 | auto languageTag = L"LanguageName:" + languageName; 183 | 184 | for(auto& character : languageTag) { 185 | character = std::tolower(character, std::locale::classic()); 186 | } 187 | 188 | auto string = data.table.find(languageTag); 189 | if(string == data.table.end()) { 190 | static auto error = std::wstring{L""}; 191 | return error; 192 | } 193 | return string->second; 194 | } 195 | 196 | void displayErrorMessage(const std::exception& error, const LanguageData& languageData) { 197 | auto message = getText(languageData, errorMessage); 198 | message += L"\r\n"; 199 | message += toWide(error.what()); 200 | MessageBoxW(nullptr, 201 | message.c_str(), 202 | getText(languageData, captionString).c_str(), 203 | MB_ICONERROR|MB_OK); 204 | } 205 | 206 | void notifyCantUpdate(const LanguageData& languageData) { 207 | MessageBoxW(nullptr, 208 | getText(languageData, useOriginalRA3ToUpdate).c_str(), 209 | getText(languageData, useOriginalRA3Title).c_str(), 210 | MB_ICONINFORMATION|MB_OK); 211 | } 212 | 213 | 214 | void notifyIsNotReplay(const LanguageData& languageData) { 215 | MessageBoxW(nullptr, 216 | getText(languageData, replayCantBeParsedText).c_str(), 217 | getText(languageData, replayCantBePlayed).c_str(), 218 | MB_ICONINFORMATION|MB_OK); 219 | } 220 | 221 | bool askUserIfFixReplay(const std::wstring& replayName, const LanguageData& languageData) { 222 | auto text = getText(languageData, replayNeedsToBeFixedText) + L"\r\n" 223 | + getText(languageData, fixReplayWarning); 224 | auto answer = MessageBoxW(nullptr, 225 | text.c_str(), 226 | getText(languageData, replayCantBePlayed).c_str(), 227 | MB_ICONWARNING|MB_YESNO); 228 | return answer == IDYES; 229 | } 230 | 231 | void notifyFixReplaySucceeded(const LanguageData& languageData) { 232 | MessageBoxW(nullptr, 233 | getText(languageData, fixReplaySucceeded).c_str(), 234 | getText(languageData, fixReplay).c_str(), 235 | MB_ICONINFORMATION|MB_OK); 236 | } 237 | 238 | void notifyFixReplayFailed(const std::exception& error, const LanguageData& languageData) { 239 | MessageBoxW(nullptr, 240 | (getText(languageData, fixReplayFailed) + L"\r\n" + toWide(error.what())).c_str(), 241 | getText(languageData, fixReplay).c_str(), 242 | MB_ICONERROR|MB_OK); 243 | } 244 | 245 | void notifyNoCommentatorAvailable(const LanguageData& languageData) { 246 | MessageBoxW(nullptr, 247 | getText(languageData, replayDontHaveCommentator).c_str(), 248 | getText(languageData, replayCantBePlayed).c_str(), 249 | MB_ICONINFORMATION|MB_OK); 250 | } 251 | 252 | void notifyGameVersionNotFound(const std::wstring& gameVersion, const LanguageData& languageData) { 253 | constexpr auto placeholder = std::wstring_view{L"%s"}; 254 | auto message = getText(languageData, gameVersionNotFound); 255 | message.replace(message.find(placeholder), placeholder.size(), gameVersion); 256 | MessageBoxW(nullptr, message.c_str(), getText(languageData, captionString).c_str(), MB_ICONERROR|MB_OK); 257 | } 258 | 259 | void notifyModNotFound(const LanguageData& languageData) { 260 | MessageBoxW(nullptr, 261 | getText(languageData, noModsFound).c_str(), 262 | getText(languageData, captionString).c_str(), 263 | MB_ICONINFORMATION|MB_OK); 264 | } 265 | void notifyReplayModNotFound(const LanguageData& languageData) { 266 | MessageBoxW(nullptr, 267 | getText(languageData, noModsFound).c_str(), 268 | getText(languageData, replayCantBePlayed).c_str(), 269 | MB_ICONINFORMATION|MB_OK); 270 | } 271 | void notifyReplayModAmbiguity(const LanguageData& languageData) { 272 | MessageBoxW(nullptr, 273 | getText(languageData, multipleModsFound).c_str(), 274 | getText(languageData, replayCantBePlayed).c_str(), 275 | MB_ICONINFORMATION|MB_OK); 276 | } 277 | 278 | DWORD colorMultiply (DWORD color, std::uint8_t multiplier) { 279 | auto result = DWORD{}; 280 | for(auto i = 0; i < 3; ++i) { 281 | auto component = ((color >> (i * 8)) bitand 0xFFu) / static_cast(0xFFu); 282 | auto finalComponent = static_cast(component * multiplier); 283 | result = result bitor (finalComponent << (i * 8)); 284 | } 285 | return result; 286 | } 287 | 288 | LanguageData getNewLanguage(HWND controlCenter, const std::wstring& ra3Path, HICON icon, const LanguageData& languageData); 289 | std::optional runGameBrowser(HWND controlCenter, const std::wstring& ra3Path, HICON icon, const LanguageData& languageData); 290 | void aboutWindow(HWND parent, HICON icon, const LanguageData& languageData); 291 | 292 | void centerDialogBox (HWND parent, HWND dialogBox, UINT additionalFlags = 0) { 293 | auto parentRect = getWindowRect(parent); 294 | auto dialogBoxRect = getWindowRect(dialogBox); 295 | auto [left, top, right, bottom] = getWindowRect(GetDesktopWindow()); 296 | 297 | auto halfWidth = (rectWidth(parentRect) - rectWidth(dialogBoxRect)) / 2; 298 | auto halfHeight = (rectHeight(parentRect) - rectHeight(dialogBoxRect)) / 2; 299 | auto [x, y] = std::pair{parentRect.left + halfWidth, parentRect.top + halfHeight}; 300 | 301 | if(x < left) x = left; 302 | if(y + rectHeight(dialogBoxRect) > bottom) y = bottom - rectHeight(dialogBoxRect); 303 | if(x + rectWidth(dialogBoxRect) > right) x = right - rectWidth(dialogBoxRect); 304 | if(y < top) y = top; 305 | 306 | SetWindowPos(dialogBox, nullptr, x, y, rectWidth(dialogBoxRect), rectHeight(dialogBoxRect), 307 | SWP_NOZORDER|additionalFlags) >> checkWin32Result("SetWindowPos", errorValue, false); 308 | } 309 | 310 | void myBeginDialog(HWND dialogBox, const std::wstring& title, HICON icon, HWND center, DWORD commonControls, RECT clientArea) { 311 | auto controlsToBeInitialized = INITCOMMONCONTROLSEX{ sizeof(INITCOMMONCONTROLSEX), commonControls }; 312 | InitCommonControlsEx(&controlsToBeInitialized) >> checkWin32Result("InitCommonControlsEx", errorValue, FALSE); 313 | 314 | auto realSize = setWindowRectByClientRect(dialogBox, rectWidth(clientArea), rectHeight(clientArea)); 315 | SetWindowPos(dialogBox, nullptr, 0, 0, rectWidth(realSize), rectHeight(realSize), SWP_NOMOVE|SWP_NOZORDER) 316 | >> checkWin32Result("SetWindowPos", errorValue, false); 317 | 318 | centerDialogBox(center, dialogBox); 319 | 320 | SetWindowTextW(dialogBox, title.c_str()) >> checkWin32Result("SetWindowTextW", errorValue, false); 321 | 322 | SendMessageW(dialogBox, WM_SETICON, ICON_BIG, reinterpret_cast(icon)); 323 | SendMessageW(dialogBox, WM_SETICON, ICON_SMALL, reinterpret_cast(icon)); 324 | } 325 | 326 | FontHandle getNormalFont(std::optional fontHeight = std::nullopt) { 327 | auto metrics = NONCLIENTMETRICSW{0}; 328 | metrics.cbSize = sizeof(NONCLIENTMETRICSW); 329 | SystemParametersInfoW(SPI_GETNONCLIENTMETRICS, sizeof(NONCLIENTMETRICSW), &metrics, 0) 330 | >> checkWin32Result("SystemParametersInfoW", errorValue, FALSE); 331 | auto& logicalFont = metrics.lfMessageFont; 332 | if(fontHeight.has_value()) { 333 | logicalFont.lfHeight = fontHeight.value(); 334 | logicalFont.lfWidth = 0; 335 | } 336 | return createFontIndirect(logicalFont); 337 | } 338 | 339 | int getRealFontHeight(HDC deviceContext, HFONT font) { 340 | auto textMetrics = TEXTMETRIC{}; 341 | auto previouslySelected = selectObject(deviceContext, font); 342 | GetTextMetrics(deviceContext, &textMetrics) >> checkWin32Result("GetTextMetrics", errorValue, false); 343 | return textMetrics.tmHeight; 344 | } 345 | 346 | FontHandle getAdjustedNormalFont(HDC deviceContext, int height) { 347 | auto adjustedHeight = height * height / getRealFontHeight(deviceContext, getNormalFont(height).get()); 348 | return getNormalFont(adjustedHeight); 349 | } 350 | 351 | BitmapHandle loadImageFromLauncherPath(const std::wstring& ra3Path, std::wstring_view item, const RECT& imageRect) { 352 | return loadImage((ra3Path + L"Launcher\\").append(item.data(), item.size()), {rectWidth(imageRect), rectHeight(imageRect)}); 353 | }; 354 | 355 | template 356 | typename std::conditional_t, 357 | std::invoke_result, 358 | std::enable_if, ValueGetter> 359 | >::type 360 | tryWith(ValueGetter&& valueGetter, Alternatives&&... alternatives) { 361 | try { 362 | if constexpr(std::is_invocable_v) { 363 | return valueGetter(); 364 | } 365 | else { 366 | return std::forward(valueGetter); 367 | } 368 | } 369 | catch(...) { 370 | if constexpr(sizeof...(alternatives) == 0) { 371 | throw std::runtime_error("Alternatives exhausted."); 372 | } 373 | else { 374 | return tryWith(std::forward(alternatives)...); 375 | } 376 | } 377 | } 378 | 379 | SplashScreenResult displaySplashScreen(const std::wstring& ra3Path, const LanguageData& languageData) { 380 | static constexpr auto splashRect = RECT{0, 0, 640, 480}; 381 | static constexpr auto noticeBarRect = RECT{ 382 | 0, 383 | rectHeight(splashRect) * 75 / 100, 384 | rectWidth(splashRect), 385 | rectHeight(splashRect) * 80 / 100 386 | }; 387 | constexpr auto timerInterval = 30; 388 | constexpr auto velocity = 1; 389 | constexpr auto lifeSpan = rectWidth(splashRect) * 3; 390 | constexpr auto virtualWidth = rectWidth(splashRect) * 2; 391 | 392 | auto icon = tryWith([&ra3Path] { return loadImage(ra3Path + L"ra3.ico"); }, 393 | [] { return loadImage(GetModuleHandle(nullptr), DEFAULT_ICON); }); 394 | auto backgroundBrush = BrushHandle{nullptr}; 395 | auto backgroundBrushWithText = BrushHandle{nullptr}; 396 | auto lastTickCount = GetTickCount(); 397 | auto edge = LONG{0}; 398 | 399 | auto handlers = ModalDialogBox::HandlerTable{}; 400 | 401 | auto initializeBrushes = [&ra3Path, &languageData, &backgroundBrush, &backgroundBrushWithText](HDC deviceContext) { 402 | using std::bind; 403 | using std::ref; 404 | auto backgroundLoader = bind(loadImageFromLauncherPath, ref(ra3Path), std::placeholders::_1, ref(splashRect)); 405 | auto background = tryWith(bind(backgroundLoader, languageData.languageName + L"_splash.bmp"), 406 | bind(backgroundLoader, L"splash.bmp"), 407 | createCompatibleBitmap(deviceContext, rectWidth(splashRect), rectHeight(splashRect))); 408 | backgroundBrush = createPatternBrush(background.get()); 409 | 410 | auto bitmapBits = getBitmapBits(deviceContext, background.get(), {{rectWidth(splashRect), -rectHeight(splashRect)}}); 411 | for(auto y = noticeBarRect.top; y < noticeBarRect.bottom; ++y) { 412 | auto row = y * rectWidth(splashRect); 413 | for(auto column = noticeBarRect.left; column < noticeBarRect.right; ++column) { 414 | auto& pixel = bitmapBits.buffer.at(row + column); 415 | pixel = colorMultiply(pixel, 0x7F); 416 | } 417 | } 418 | setBitmapBits(deviceContext, background.get(), bitmapBits); 419 | 420 | { 421 | auto compatibleContext = createCompatibleDeviceContext(deviceContext); 422 | auto previousBitmap = selectObject(compatibleContext.get(), background.get()); 423 | auto font = getAdjustedNormalFont(compatibleContext.get(), rectHeight(noticeBarRect)); 424 | auto previousFont = selectObject(compatibleContext.get(), font.get()); 425 | auto previousMode = setBackgroundMode(compatibleContext.get(), TRANSPARENT); 426 | auto previousTextColor = setTextColor(compatibleContext.get(), RGB(255, 255, 255)); 427 | auto textRect = noticeBarRect; 428 | DrawTextW(compatibleContext.get(), getText(languageData, splashScreen).c_str(), -1, &textRect, DT_CENTER) 429 | >> checkWin32Result("DrawTextW", errorValue, 0); 430 | } 431 | 432 | backgroundBrushWithText = createPatternBrush(background.get()); 433 | }; 434 | 435 | handlers[WM_INITDIALOG] = [&languageData, &icon, &lastTickCount, initializeBrushes](HWND window, WPARAM, LPARAM) { 436 | myBeginDialog(window, getText(languageData, captionString), icon.get(), 437 | GetDesktopWindow(), ICC_STANDARD_CLASSES, splashRect); 438 | initializeBrushes(getDeviceContext(window)->context); 439 | 440 | SetTimer(window, splashScreen, timerInterval, nullptr) >> checkWin32Result("SetTimer", errorValue, false); 441 | return TRUE; 442 | }; 443 | 444 | handlers[WM_TIMER] = [&lastTickCount, &edge](HWND window, WPARAM timerID, LPARAM) { 445 | if(timerID == splashScreen) { 446 | auto oldTickCount = lastTickCount; 447 | lastTickCount = GetTickCount(); 448 | edge += (lastTickCount - oldTickCount) * velocity; 449 | if(edge > lifeSpan) { 450 | KillTimer(window, timerID) >> checkWin32Result("KillTimer", errorValue, false); 451 | EndDialog(window, static_cast(SplashScreenResult::normal)) >> checkWin32Result("EndDialog", errorValue, false); 452 | } 453 | InvalidateRect(window, ¬iceBarRect, false) >> checkWin32Result("InvalidateRect", errorValue, false); 454 | return TRUE; 455 | } 456 | return FALSE; 457 | }; 458 | 459 | handlers[WM_LBUTTONDOWN] = [](HWND window, WPARAM, LPARAM) { 460 | EndDialog(window, static_cast(SplashScreenResult::clicked)) >> checkWin32Result("EndDialog", errorValue, false); 461 | return TRUE; 462 | }; 463 | 464 | auto drawer = [&backgroundBrush, &backgroundBrushWithText, &edge](HDC context, const RECT& dirtyRect) { 465 | auto textRect = dirtyRect; 466 | textRect.left = std::clamp(edge, dirtyRect.left, dirtyRect.right); 467 | textRect.right = std::clamp(edge - virtualWidth, dirtyRect.left, dirtyRect.right); 468 | auto remainedLeft = dirtyRect; 469 | auto remainedRight = dirtyRect; 470 | remainedLeft.right = textRect.left; 471 | remainedRight.left = textRect.right; 472 | 473 | FillRect(context, &remainedLeft, backgroundBrush.get()) >> checkWin32Result("FillRect", errorValue, false); 474 | FillRect(context, &remainedRight, backgroundBrush.get()) >> checkWin32Result("FillRect", errorValue, false); 475 | FillRect(context, &textRect, backgroundBrushWithText.get()) >> checkWin32Result("FillRect", errorValue, false); 476 | }; 477 | 478 | handlers[WM_PAINT] = [drawer](HWND window, WPARAM, LPARAM) { 479 | auto paint = beginPaint(window); 480 | auto dirtyRect = paint->paintStruct.rcPaint; 481 | if(paint->paintStruct.fErase) { 482 | dirtyRect = splashRect; 483 | } 484 | auto memoryContext = createCompatibleDeviceContext(paint->context); 485 | auto compatibleBitmap = createCompatibleBitmap(paint->context, rectWidth(splashRect), rectHeight(splashRect)); 486 | auto previousMemoryBitmap = selectObject(memoryContext.get(), compatibleBitmap.get()); 487 | 488 | drawer(memoryContext.get(), dirtyRect); 489 | auto [x, y, width, height] = std::tuple{dirtyRect.left, dirtyRect.top, rectWidth(dirtyRect), rectHeight(dirtyRect)}; 490 | BitBlt(paint->context, x, y, width, height, memoryContext.get(), x, y, SRCCOPY) 491 | >> checkWin32Result("BitBlt", errorValue, false); 492 | return TRUE; 493 | }; 494 | 495 | return static_cast(modalDialogBox(handlers, WS_VISIBLE|WS_POPUP, 0)); 496 | } 497 | 498 | std::optional runControlCenter(const std::wstring& ra3Path, const std::wstring& userCommandLine, LanguageData& languageData, HBITMAP customBackground) { 499 | 500 | constexpr auto firstLineStart = 67; 501 | constexpr auto secondLineStart = 139; 502 | 503 | constexpr auto commandLineY = 470; 504 | constexpr auto commandLineWidth = 800 - 2 * firstLineStart; 505 | constexpr auto commandLineHeight = 20; 506 | constexpr auto commandLineTitleY = commandLineY - commandLineHeight - 2; 507 | 508 | constexpr auto buttonWidth = 121; 509 | constexpr auto buttonHeight = 34; 510 | constexpr auto buttonPadding = 12; 511 | 512 | static constexpr auto clientArea = RECT{0, 0, 800, 600}; 513 | static constexpr auto allButtons = { 514 | playGame, checkForUpdates, setLanguage, gameBrowser, readMe, 515 | visitEAWebsite, technicalSupport, deauthorize, quit, about, 516 | }; 517 | 518 | auto icon = tryWith([&ra3Path] { return loadImage(ra3Path + L"ra3.ico"); }, 519 | [] { return loadImage(GetModuleHandle(nullptr), DEFAULT_ICON); }); 520 | 521 | auto backgroundBrushes = std::array {}; 522 | const auto& [mainBackground, commandLineBackground] = backgroundBrushes; 523 | 524 | auto buttonBrushes = std::array {}; 525 | auto buttonFont = getNormalFont(); 526 | constexpr auto buttonTextFormats = DT_CENTER|DT_SINGLELINE|DT_VCENTER; 527 | auto commandLineFont = getNormalFont(commandLineHeight); 528 | 529 | auto handlers = ModalDialogBox::HandlerTable{}; 530 | auto returnValue = std::optional {}; 531 | 532 | auto setUpBrushes = [&ra3Path, &languageData, customBackground, &backgroundBrushes, &buttonBrushes](HDC deviceContext) { 533 | using std::bind; 534 | using std::ref; 535 | auto backgroundLoader = bind(loadImageFromLauncherPath, ref(ra3Path), std::placeholders::_1, ref(clientArea)); 536 | auto background = tryWith(bind(backgroundLoader, languageData.languageName + L"_cnc.bmp"), 537 | bind(backgroundLoader, L"cnc.bmp"), 538 | createCompatibleBitmap(deviceContext, rectWidth(clientArea), rectHeight(clientArea))); 539 | auto rawBackground = customBackground ? customBackground : background.get(); 540 | backgroundBrushes[0] = createPatternBrush(rawBackground); 541 | 542 | auto bitmapBits = getBitmapBits(deviceContext, rawBackground, {{rectWidth(clientArea), -rectHeight(clientArea)}}); 543 | for(auto y = commandLineY; y < commandLineY + commandLineHeight; ++y) { 544 | auto row = y * rectWidth(clientArea); 545 | for(auto column = firstLineStart; column < firstLineStart + commandLineWidth; ++column) { 546 | auto sp = (y - commandLineY) * rectWidth(clientArea) + (column - firstLineStart); 547 | bitmapBits.buffer.at(sp) = colorMultiply(bitmapBits.buffer.at(row + column), 150); 548 | } 549 | } 550 | auto duplicate = createCompatibleBitmap(deviceContext, rectWidth(clientArea), rectHeight(clientArea)); 551 | setBitmapBits(deviceContext, duplicate.get(), bitmapBits); 552 | backgroundBrushes[1] = createPatternBrush(duplicate.get()); 553 | 554 | auto maxDistance = 10; 555 | auto greyButton = std::vector {}; 556 | greyButton.resize(buttonWidth * buttonHeight); 557 | for(auto y = 0; y < buttonHeight; ++y) { 558 | auto row = y * buttonWidth; 559 | for(auto column = 0; column < buttonWidth; ++column) { 560 | auto [min, max] = std::minmax({std::min(column, buttonWidth - column), std::min(y, buttonHeight - y)}); 561 | min = std::clamp(min / maxDistance, 0.0, 1.0); 562 | max = std::clamp(max / maxDistance, 0.0, 1.0); 563 | auto value = std::clamp(min + (2.0 - max) * max - 1.0, 0.0, 1.0); 564 | greyButton.at(row + column) = std::clamp(255 * ((2.0 * value - 3.0) * value * value + 1.0), 0, 255); 565 | } 566 | } 567 | 568 | auto getButtonBrush = [deviceContext, &greyButton](DWORD backgroundColor, DWORD borderColor) { 569 | auto buttonBitmap = createCompatibleBitmap(deviceContext, buttonWidth, buttonHeight); 570 | auto bitmapBits = getBitmapBits(deviceContext, buttonBitmap.get()); 571 | for(auto i = std::size_t{0}; i < bitmapBits.buffer.size(); ++i) { 572 | auto& color = bitmapBits.buffer.at(i); 573 | auto horizontalBorder = (i < buttonWidth) or (i / buttonWidth == buttonHeight - 1); 574 | auto verticalBorder = (i % buttonWidth == 0) or (i % buttonWidth == buttonWidth - 1); 575 | 576 | if(verticalBorder and horizontalBorder) { 577 | color = colorMultiply(borderColor, 0x7F); 578 | } 579 | else if(verticalBorder or horizontalBorder) { 580 | color = colorMultiply(borderColor, 0xFF); 581 | } 582 | else { 583 | color = colorMultiply(backgroundColor, greyButton.at(i)); 584 | } 585 | } 586 | setBitmapBits(deviceContext, buttonBitmap.get(), bitmapBits); 587 | return createPatternBrush(buttonBitmap.get()); 588 | }; 589 | 590 | buttonBrushes[0] = getButtonBrush(RGB(12, 29, 129), RGB(0, 118, 230)); 591 | buttonBrushes[1] = getButtonBrush(RGB(0, 230, 222), RGB(230, 230, 230)); 592 | buttonBrushes[2] = getButtonBrush(RGB(0, 157, 230), RGB(0, 118, 230)); 593 | }; 594 | 595 | auto initializeCommandLineWindow = [&userCommandLine, &commandLineFont](HWND window) { 596 | auto commandLineWindow = createControl(window, commandLine, WC_EDITW, userCommandLine.c_str(), 597 | WS_VISIBLE|WS_BORDER|ES_AUTOHSCROLL, WS_EX_TRANSPARENT, 598 | firstLineStart, commandLineY, commandLineWidth, commandLineHeight).release(); 599 | commandLineFont = getAdjustedNormalFont(getDeviceContext(window)->context, commandLineHeight); 600 | SendMessageW(commandLineWindow, WM_SETFONT, reinterpret_cast(commandLineFont.get()), true); 601 | }; 602 | 603 | auto adjustButtonFont = [&languageData, &buttonFont](HWND window) { 604 | auto getTextRect = [&buttonFont](HDC deviceContext, std::wstring_view text) { 605 | auto textRect = RECT{0, 0, buttonWidth, buttonHeight}; 606 | auto previousFont = selectObject(deviceContext, buttonFont.get()); 607 | DrawTextW(deviceContext, text.data(), text.size(), &textRect, buttonTextFormats|DT_CALCRECT) 608 | >> checkWin32Result("DrawText", errorValue, 0); 609 | return textRect; 610 | }; 611 | 612 | buttonFont = getNormalFont(); 613 | for(auto id : allButtons) { 614 | auto button = getControlByID(window, id); 615 | InvalidateRect(button, nullptr, false) >> checkWin32Result("InvalidateRect", errorValue, false); 616 | 617 | auto context = getDeviceContext(button); 618 | const auto& text = getText(languageData, id); 619 | auto textRect = getTextRect(context->context, text); 620 | if(rectWidth(textRect) <= buttonWidth) { 621 | continue; 622 | } 623 | auto realHeight = getRealFontHeight(context->context, buttonFont.get()); 624 | auto adjustedHeight = realHeight * buttonWidth / rectWidth(textRect); 625 | buttonFont = getAdjustedNormalFont(context->context, adjustedHeight); 626 | } 627 | }; 628 | 629 | handlers[WM_INITDIALOG] = [&languageData, &icon, setUpBrushes, initializeCommandLineWindow, adjustButtonFont](HWND window, WPARAM, LPARAM) { 630 | myBeginDialog(window, getText(languageData, captionString), icon.get(), 631 | GetDesktopWindow(), ICC_STANDARD_CLASSES, clientArea); 632 | 633 | setUpBrushes(getDeviceContext(window)->context); 634 | 635 | initializeCommandLineWindow(window); 636 | 637 | auto currentX = 0; 638 | auto nextX = [¤tX] { return currentX += buttonWidth + buttonPadding; }; 639 | 640 | currentX = firstLineStart - (buttonWidth + buttonPadding); 641 | for(auto id : {playGame, checkForUpdates, setLanguage, gameBrowser, readMe}) { 642 | createControl(window, id, WC_BUTTONW, {}, WS_VISIBLE, 0, 643 | nextX(), 520, buttonWidth, buttonHeight).release(); 644 | } 645 | 646 | currentX = secondLineStart - (buttonWidth + buttonPadding); 647 | for(auto id : {visitEAWebsite, technicalSupport, deauthorize, quit}) { 648 | createControl(window, id, WC_BUTTONW, {}, WS_VISIBLE, 0, 649 | nextX(), 562, buttonWidth, buttonHeight).release(); 650 | } 651 | 652 | createControl(window, about, WC_BUTTONW, {}, WS_VISIBLE, 0, 653 | clientArea.right - buttonWidth - buttonPadding, clientArea.top + buttonPadding, 654 | buttonWidth, buttonHeight).release(); 655 | 656 | adjustButtonFont(window); 657 | return TRUE; 658 | }; 659 | 660 | handlers[WM_CTLCOLOREDIT] = [&commandLineBackground](HWND window, WPARAM hdcAddress, LPARAM childAddress) -> INT_PTR { 661 | auto childWindow = reinterpret_cast(childAddress); 662 | if(getControlID(childWindow) != commandLine) { 663 | return FALSE; 664 | } 665 | auto childDC = reinterpret_cast(hdcAddress); 666 | setBackgroundMode(childDC, TRANSPARENT).release(); 667 | setTextColor(childDC, RGB(255, 255, 255)).release(); 668 | return reinterpret_cast(commandLineBackground.get()); //per https://msdn.microsoft.com/en-us/library/windows/desktop/bb761691(v=vs.85).aspx 669 | }; 670 | 671 | 672 | handlers[WM_PAINT] = [&languageData, &mainBackground, &commandLineFont](HWND window, WPARAM, LPARAM) { 673 | auto paint = beginPaint(window); 674 | auto previousMode = setBackgroundMode(paint->context, TRANSPARENT); 675 | auto previousTextColor = setTextColor(paint->context, RGB(255, 255, 255)); 676 | auto previouslySelected = selectObject(paint->context, commandLineFont.get()); 677 | 678 | FillRect(paint->context, &paint->paintStruct.rcPaint, mainBackground.get()) >> checkWin32Result("FillRect", errorValue, false); 679 | TextOutW(paint->context, firstLineStart, commandLineTitleY, 680 | getText(languageData, commandLine).c_str(), getText(languageData, commandLine).size()) >> checkWin32Result("TextOut", errorValue, false); 681 | return TRUE; 682 | }; 683 | 684 | auto getCommandLines = [](HWND controlCenter) { 685 | auto commandLineWindow = getControlByID(controlCenter, commandLine); 686 | auto length = GetWindowTextLengthW(commandLineWindow); 687 | auto windowString = std::wstring{static_cast(length + 1), L'\0', std::wstring::allocator_type{}}; 688 | auto realLength = GetWindowTextW(commandLineWindow, windowString.data(), length + 1); 689 | windowString.resize(realLength); 690 | return windowString; 691 | }; 692 | 693 | auto endControlCenter = [&returnValue](HWND window, std::optional launchOptions) { 694 | returnValue = std::move(launchOptions); 695 | EndDialog(window, 0) >> checkWin32Result("EndDialog", errorValue, false); 696 | }; 697 | 698 | handlers[WM_COMMAND] = [&ra3Path, &languageData, &icon, setUpBrushes, adjustButtonFont, getCommandLines, endControlCenter](HWND window, WPARAM codeAndIdentifier, LPARAM childWindow) { 699 | auto notificationCode = HIWORD(codeAndIdentifier); 700 | auto identifier = LOWORD(codeAndIdentifier); 701 | if(notificationCode != BN_CLICKED) { 702 | return FALSE; 703 | } 704 | switch(identifier) { 705 | case playGame: { 706 | endControlCenter(window, LaunchOptions{LaunchOptions::noFile, {}, getCommandLines(window)}); 707 | break; 708 | } 709 | case checkForUpdates: { 710 | notifyCantUpdate(languageData); 711 | break; 712 | } 713 | case setLanguage: { 714 | languageData = getNewLanguage(window, ra3Path, icon.get(), languageData); 715 | SetWindowTextW(window, getText(languageData, captionString).c_str()) >> checkWin32Result("SetWindowTextW", successValue, true); 716 | setUpBrushes(getDeviceContext(window)->context); 717 | adjustButtonFont(window); 718 | InvalidateRect(window, nullptr, false) >> checkWin32Result("InvalidateRect", errorValue, false); 719 | break; 720 | } 721 | case gameBrowser: { 722 | auto launchOptions = runGameBrowser(window, ra3Path, icon.get(), languageData); 723 | if(launchOptions.has_value()) { 724 | launchOptions->extraCommandLine = getCommandLines(window); 725 | endControlCenter(window, std::move(launchOptions)); 726 | } 727 | break; 728 | } 729 | case readMe: { 730 | try { 731 | shellExecute(window, L"open", getRegistryString(getRa3RegistryKey(HKEY_LOCAL_MACHINE).get(), L"Readme")); 732 | } 733 | catch(...) { } 734 | break; 735 | } 736 | case visitEAWebsite: { 737 | shellExecute(window, L"open", getText(languageData, webSiteLink)); 738 | break; 739 | } 740 | case technicalSupport: { 741 | shellExecute(window, L"open", getText(languageData, eaSupportURL)); 742 | break; 743 | } 744 | case deauthorize: { 745 | MessageBoxW(window, 746 | getText(languageData, useOriginalRA3ToDeauthorize).c_str(), 747 | getText(languageData, useOriginalRA3Title).c_str(), 748 | MB_ICONINFORMATION|MB_OK); 749 | break; 750 | } 751 | case about: { 752 | aboutWindow(window, icon.get(), languageData); 753 | break; 754 | } 755 | case quit: { 756 | endControlCenter(window, std::nullopt); 757 | break; 758 | } 759 | } 760 | return TRUE; 761 | 762 | }; 763 | 764 | 765 | 766 | handlers[WM_NOTIFY] = [&languageData, &buttonBrushes, &buttonFont](HWND window, WPARAM, LPARAM dataHeaderAddress) { 767 | const auto& dataHeader = *reinterpret_cast(dataHeaderAddress); 768 | auto id = static_cast(dataHeader.idFrom); 769 | 770 | if(std::count(std::begin(allButtons), std::end(allButtons), id) == 0) { 771 | return FALSE; 772 | } 773 | 774 | if(dataHeader.code != NM_CUSTOMDRAW) { 775 | return FALSE; 776 | } 777 | 778 | const auto& customDraw = *reinterpret_cast(dataHeaderAddress); 779 | 780 | if(customDraw.dwDrawStage != CDDS_PREPAINT) { 781 | return FALSE; 782 | } 783 | 784 | const auto& buttonText = getText(languageData, id); 785 | 786 | auto previousMode = setBackgroundMode(customDraw.hdc, TRANSPARENT); 787 | auto previousColor = setTextColor(customDraw.hdc, RGB(255, 255, 255)); 788 | 789 | auto buttonState = SendMessageW(dataHeader.hwndFrom, BM_GETSTATE, 0, 0); 790 | auto brush = HBRUSH{nullptr}; 791 | if(buttonState bitand BST_PUSHED) { 792 | brush = buttonBrushes[2].get(); 793 | } 794 | else if(buttonState bitand BST_HOT) { 795 | brush = buttonBrushes[1].get(); 796 | } 797 | else { 798 | brush = buttonBrushes[0].get(); 799 | } 800 | 801 | auto buttonRect = getClientRect(dataHeader.hwndFrom); 802 | 803 | if(brush != nullptr) { 804 | FillRect(customDraw.hdc, &buttonRect, brush) >> checkWin32Result("FillRect", errorValue, false); 805 | } 806 | 807 | auto previousFont = selectObject(customDraw.hdc, buttonFont.get()); 808 | DrawTextW(customDraw.hdc, buttonText.c_str(), buttonText.size(), &buttonRect, buttonTextFormats); 809 | return CDRF_SKIPDEFAULT; 810 | }; 811 | 812 | handlers[WM_CLOSE] = [endControlCenter](HWND window, WPARAM, LPARAM) { 813 | endControlCenter(window, std::nullopt); 814 | return TRUE; 815 | }; 816 | 817 | modalDialogBox(handlers, WS_VISIBLE|WS_SYSMENU|WS_MINIMIZEBOX, WS_EX_OVERLAPPEDWINDOW); 818 | return returnValue; 819 | } 820 | 821 | LanguageData getNewLanguage(HWND controlCenter, const std::wstring& ra3Path, HICON icon, const LanguageData& languageData) { 822 | 823 | constexpr auto buttonWidth = 100; 824 | constexpr auto buttonHeight = 30; 825 | constexpr auto buttonPadding = 10; 826 | 827 | static constexpr auto bannerRect = RECT{0, 0, 480, 100}; 828 | constexpr auto clientArea = RECT{0, 0, rectWidth(bannerRect), rectHeight(bannerRect) + 3 * buttonHeight + 2 * buttonPadding}; 829 | 830 | constexpr auto textX = buttonPadding; 831 | constexpr auto textY = bannerRect.bottom; 832 | constexpr auto textWidth = rectWidth(clientArea) - 2 * textX; 833 | constexpr auto textHeight = buttonHeight; 834 | 835 | constexpr auto listWidth = rectWidth(clientArea) * 0.75; 836 | constexpr auto listHeight = buttonHeight; 837 | constexpr auto listX = (rectWidth(clientArea) - listWidth) / 2; 838 | constexpr auto listY = textY + textHeight; 839 | 840 | constexpr auto buttonX = buttonPadding; 841 | constexpr auto buttonY = listY + listHeight + buttonPadding; 842 | 843 | 844 | auto bannerLoader = std::bind(loadImageFromLauncherPath, std::ref(ra3Path), std::placeholders::_1, std::ref(bannerRect)); 845 | auto banner480 = tryWith(std::bind(bannerLoader, languageData.languageName + L"_480banner.bmp"), 846 | std::bind(bannerLoader, L"480banner.bmp"), 847 | nullptr); 848 | auto banner480Brush = tryWith(std::bind(createPatternBrush, banner480.get()), nullptr); 849 | auto font = getNormalFont(); 850 | 851 | auto languages = getAllLanguages(ra3Path); 852 | if(auto current = std::find(std::begin(languages), std::end(languages), languageData.languageName); 853 | current != std::end(languages) and current != std::begin(languages)) { 854 | std::swap(*current, *std::begin(languages)); 855 | } 856 | 857 | auto handlers = ModalDialogBox::HandlerTable{}; 858 | 859 | handlers[WM_INITDIALOG] = [controlCenter, icon, &languageData, &font, &languages](HWND dialogBox, WPARAM wParam, LPARAM lParam) { 860 | myBeginDialog(dialogBox, getText(languageData, setLanguage), icon, 861 | controlCenter, ICC_STANDARD_CLASSES, clientArea); 862 | 863 | auto list = createControl(dialogBox, setLanguageList, WC_COMBOBOXW, {}, 864 | WS_VISIBLE|CBS_DROPDOWNLIST, 0, listX, listY, listWidth, 0).release(); 865 | for(auto i = std::size_t{0}; i < languages.size(); ++i) { 866 | SendMessageW(list, CB_ADDSTRING, 0, reinterpret_cast(getLanguageName(languageData, languages[i]).c_str())) 867 | >> checkWin32Result("SendMessageW CB_ADDSTRINGW", successValue, static_cast(i)); 868 | } 869 | SendMessageW(list, CB_SETCURSEL, 0, 0) >> checkWin32Result("SendMessageW CB_SETCURSEL", successValue, 0); 870 | 871 | createControl(dialogBox, setLanguageDescription, WC_STATICW, getText(languageData, setLanguageDescription).c_str(), 872 | WS_VISIBLE|SS_LEFT|SS_CENTERIMAGE, 0, textX, textY, textWidth, textHeight).release(); 873 | 874 | auto buttonOffset = buttonX; 875 | for(auto id : {setLanguageOK, setLanguageCancel}) { 876 | createControl(dialogBox, id, WC_BUTTONW, getText(languageData, id), 877 | WS_VISIBLE, 0, buttonOffset, buttonY, buttonWidth, buttonHeight).release(); 878 | buttonOffset += buttonWidth + buttonPadding; 879 | } 880 | 881 | for(auto id : {setLanguageList, setLanguageDescription, setLanguageOK, setLanguageCancel}) { 882 | SendMessageW(getControlByID(dialogBox, id), WM_SETFONT, reinterpret_cast(font.get()), true); 883 | } 884 | return TRUE; 885 | }; 886 | 887 | handlers[WM_PAINT] = [&languageData, &banner480Brush](HWND dialogBox, WPARAM, LPARAM lParam) { 888 | if(banner480Brush.get() == nullptr) { 889 | return FALSE; 890 | } 891 | auto painter = beginPaint(dialogBox); 892 | FillRect(painter->context, &bannerRect, banner480Brush.get()) >> checkWin32Result("FillRect", errorValue, false); 893 | return TRUE; 894 | }; 895 | 896 | handlers[WM_COMMAND] = [](HWND dialogBox, WPARAM information, LPARAM childWindowHandle) { 897 | auto notificationCode = HIWORD(information); 898 | auto identifier = LOWORD(information); 899 | if(notificationCode == BN_CLICKED) { 900 | if(identifier == setLanguageOK) { 901 | auto id = SendMessageW(getControlByID(dialogBox, setLanguageList), CB_GETCURSEL, 0, 0) 902 | >> checkWin32Result("SendMessageW CB_GETCURSEL", errorValue, CB_ERR); 903 | EndDialog(dialogBox, id) >> checkWin32Result("EndDialog", errorValue, false); 904 | } 905 | if(identifier == setLanguageCancel) { 906 | EndDialog(dialogBox, 0) >> checkWin32Result("EndDialog", errorValue, false); 907 | } 908 | } 909 | return TRUE; 910 | }; 911 | 912 | handlers[WM_CLOSE] = [](HWND dialogBox, WPARAM, LPARAM) { 913 | EndDialog(dialogBox, 0) >> checkWin32Result("EndDialog", errorValue, false); 914 | return TRUE; 915 | }; 916 | 917 | auto result = modalDialogBox(handlers, WS_VISIBLE|WS_SYSMENU, 0, controlCenter); 918 | return loadLanguageData(ra3Path, languages.at(result)); 919 | }; 920 | 921 | 922 | template 923 | WindowHandle createListView(HWND parent, ID id, const IDWidthList& columns, RECT windowRect, const LanguageData& languageData) { 924 | auto listView = createControl(parent, id, WC_LISTVIEWW, {}, 925 | LVS_REPORT|LVS_SHOWSELALWAYS|LVS_SINGLESEL, 0, 926 | windowRect.left, windowRect.top, rectWidth(windowRect), rectHeight(windowRect)); 927 | SendMessageW(listView.get(), LVM_SETEXTENDEDLISTVIEWSTYLE, 0, LVS_EX_GRIDLINES | LVS_EX_FULLROWSELECT | LVS_EX_DOUBLEBUFFER); 928 | auto listViewWidth = rectWidth(getClientRect(listView.get())) - GetSystemMetrics(SM_CXVSCROLL); //minus width of scroll bar 929 | 930 | auto columnIndex = 0; 931 | for(auto [id, widthPercent] : columns) { 932 | auto buf = getText(languageData, id); 933 | auto column = LVCOLUMNW{LVCF_TEXT|LVCF_WIDTH}; 934 | column.pszText = buf.data(); 935 | column.cx = widthPercent * listViewWidth; 936 | SendMessageW(listView.get(), LVM_INSERTCOLUMNW, columnIndex, reinterpret_cast(&column)) 937 | >> checkWin32Result("SendMessageW LVM_INSERTCOLUMNW", successValue, columnIndex); 938 | 939 | columnIndex += 1; 940 | } 941 | 942 | return listView; 943 | } 944 | 945 | void replaceListView (HWND listView, std::vector> items) { 946 | SendMessageW(listView, LVM_DELETEALLITEMS, 0, 0) 947 | >> checkWin32Result("SendMessageW LVM_DELETEALLITEMS", errorValue, false); 948 | 949 | for(auto index = std::size_t{0}; index < items.size(); ++index) { 950 | for(auto subIndex = std::size_t{0}; subIndex < items[index].size(); ++subIndex) { 951 | auto listItem = LVITEMW{LVIF_TEXT}; 952 | listItem.iItem = index; 953 | listItem.iSubItem = subIndex; 954 | listItem.pszText = items[index][subIndex].data(); 955 | if(listItem.iSubItem == 0) { 956 | SendMessageW(listView, LVM_INSERTITEMW, 0, reinterpret_cast(&listItem)) 957 | >> checkWin32Result("SendMessageW LVM_INSERTITEMW", errorValue, -1); 958 | } 959 | else { 960 | SendMessageW(listView, LVM_SETITEMW, 0, reinterpret_cast(&listItem)) 961 | >> checkWin32Result("SendMessageW LVM_INSERTITEMW", errorValue, false); 962 | } 963 | } 964 | } 965 | } 966 | 967 | struct ReplaysAndModsData { 968 | 969 | std::vector> replayDetailsToStrings() const { 970 | 971 | auto allocateFormat = [](auto function, DWORD flags, const SYSTEMTIME& time) { 972 | auto size = function(LOCALE_USER_DEFAULT, flags, &time, nullptr, nullptr, 0) 973 | >> checkWin32Result("allocateFormat", errorValue, 0); 974 | auto string = std::wstring{static_cast(size), {}, std::wstring::allocator_type{}}; 975 | function(LOCALE_USER_DEFAULT, flags, &time, nullptr, string.data(), string.size()) 976 | >> checkWin32Result("allocateFormat", errorValue, 0); 977 | string.erase(string.find(L'\0')); 978 | return string; 979 | }; 980 | 981 | auto strings = std::vector> {}; 982 | for(const auto& replay : this->replayDetails) { 983 | auto fileTime = unixTimeToFileTime(replay.timeStamp); 984 | auto systemTime = SYSTEMTIME{}; 985 | FileTimeToSystemTime(&fileTime, &systemTime) 986 | >> checkWin32Result("FileTimeToSystemTime", errorValue, false); 987 | auto localTime = SYSTEMTIME{}; 988 | SystemTimeToTzSpecificLocalTime(nullptr, &systemTime, &localTime) 989 | >> checkWin32Result("SystemTimeToTzSpecificLocalTime", errorValue, false); 990 | 991 | auto date = allocateFormat(GetDateFormatW, DATE_LONGDATE, localTime); 992 | auto time = allocateFormat(GetTimeFormatW, TIME_NOSECONDS, localTime); 993 | 994 | auto gameVersion = std::to_wstring(replay.gameVersion.first) + L'.' + std::to_wstring(replay.gameVersion.second); 995 | strings.emplace_back(std::vector{replay.replayName, replay.modName, std::move(gameVersion)}); 996 | strings.back().emplace_back(date + L' ' + time); 997 | } 998 | return strings; 999 | } 1000 | 1001 | std::vector> modDetailsToStrings() const { 1002 | auto strings = std::vector> {}; 1003 | for(const auto& [fullPath, modName, modVersion] : this->modDetails) { 1004 | strings.emplace_back(std::vector{modName, modVersion}); 1005 | } 1006 | return strings; 1007 | } 1008 | 1009 | std::wstring getReplayDescription(std::size_t index, const LanguageData& languageData) const { 1010 | static constexpr auto endLine = L"\r\n"; 1011 | auto details = this->replayDetails.at(index); 1012 | auto result = std::wstring{}; 1013 | 1014 | result += getText(languageData, replayMatchInformation); 1015 | result += L" [" + details.replayName + L"] [" + this->getReplayDuration(index).value_or(getText(languageData, replayNeedsToBeFixed)) + L"]" + endLine; 1016 | result += details.title + endLine; 1017 | result += getText(languageData, replayMap) + L' ' + details.map + endLine; 1018 | result += getText(languageData, replayNumberOfPlayers); 1019 | result += L' ' + std::to_wstring(details.players.size()) + endLine; 1020 | 1021 | for(const auto& player : details.players) { 1022 | result += L' ' + player + L' '; 1023 | } 1024 | result += endLine; 1025 | 1026 | result += getText(languageData, replayDescription) + L' ' + details.description; 1027 | result += endLine; 1028 | return result; 1029 | } 1030 | 1031 | std::optional getReplayDuration(std::size_t index) const { 1032 | const auto& timeCode = this->replayDetails.at(index).finalTimeCode; 1033 | if(not timeCode.has_value()) { 1034 | return std::nullopt; 1035 | } 1036 | auto seconds = timeCode.value() / 15; 1037 | auto result = std::wstring{}; 1038 | for(const auto count : {seconds / 3600, (seconds % 3600) / 60, seconds % 60}) { 1039 | auto portion = std::to_wstring(count); 1040 | result += std::wstring{2 - portion.size(), L'0', std::wstring::allocator_type{}}; 1041 | result += portion; 1042 | result += L':'; 1043 | } 1044 | result.resize(std::min(result.size(), result.rfind(L':'))); 1045 | return result; 1046 | } 1047 | 1048 | std::vector replayDetails; 1049 | std::vector modDetails; 1050 | }; 1051 | 1052 | std::optional runGameBrowser(HWND controlCenter, const std::wstring& ra3Path, HICON icon, const LanguageData& languageData) { 1053 | using std::pair; 1054 | static constexpr auto tabs = {replays, mods}; 1055 | static constexpr auto replaySubWindows = {replayList, replayDescription, fixReplay, replayFolder}; 1056 | static constexpr auto modSubWindows = {modList, modFolder}; 1057 | static constexpr auto tabSubWindows = {pair{replays, replaySubWindows}, pair{mods, modSubWindows}}; 1058 | static constexpr auto replayListColumns = {pair{replayListReplayName, 0.52}, pair{replayListModName, 0.14}, pair{replayListGameVersion, 0.11}, pair{replayListDate, 0.23}}; 1059 | static constexpr auto modListColumns = {pair{modListModName, 0.8}, pair{modListModVersion, 0.2}}; 1060 | static constexpr auto bottomButtons = {gameBrowserLaunchGame, gameBrowserCancel}; 1061 | 1062 | constexpr auto buttonWidth = 110; 1063 | constexpr auto buttonHeight = 30; 1064 | constexpr auto buttonPadding = 5; 1065 | constexpr auto replayDescriptionHeight = 110; 1066 | 1067 | static constexpr auto bannerRect = RECT{0, 0, 640, 100}; 1068 | static constexpr auto clientArea = RECT{0, 0, rectWidth(bannerRect), 540}; 1069 | static constexpr auto page = RECT{0, rectHeight(bannerRect), rectWidth(clientArea), rectHeight(clientArea) - buttonHeight - 2 * buttonPadding}; 1070 | 1071 | auto bannerLoader = std::bind(loadImageFromLauncherPath, std::ref(ra3Path), std::placeholders::_1, std::ref(bannerRect)); 1072 | auto banner640 = tryWith(std::bind(bannerLoader, languageData.languageName + L"_640banner.bmp"), 1073 | std::bind(bannerLoader, L"640banner.bmp"), 1074 | nullptr); 1075 | auto banner640Brush = tryWith(std::bind(createPatternBrush, banner640.get()), nullptr); 1076 | auto font = getNormalFont(); 1077 | auto handlers = ModalDialogBox::HandlerTable{}; 1078 | 1079 | handlers[WM_INITDIALOG] = [controlCenter, icon, &languageData, &font](HWND dialogBox, WPARAM wParam, LPARAM lParam) { 1080 | myBeginDialog(dialogBox, getText(languageData, gameBrowser), icon, controlCenter, 1081 | ICC_STANDARD_CLASSES | ICC_TAB_CLASSES | ICC_LISTVIEW_CLASSES, clientArea); 1082 | 1083 | auto tab = createControl(dialogBox, gameBrowserTabs, WC_TABCONTROLW, {}, 1084 | WS_VISIBLE, 0, 0, page.top, rectWidth(page), rectHeight(page)).release(); 1085 | //font must be set before calculating width of tabs 1086 | SendMessageW(tab, WM_SETFONT, reinterpret_cast(font.get()), true); 1087 | 1088 | auto totaltabsWidth = 0; 1089 | auto tabIndex = 0; 1090 | for(auto id : tabs) { 1091 | auto tabItem = TCITEMW{TCIF_TEXT}; 1092 | auto buf = getText(languageData, id); 1093 | tabItem.pszText = buf.data(); 1094 | SendMessageW(tab, TCM_INSERTITEMW, tabIndex, reinterpret_cast(&tabItem)) 1095 | >> checkWin32Result("SendMessageW TCM_INSERTITEMW", successValue, tabIndex); 1096 | 1097 | auto tabRect = RECT{}; 1098 | SendMessageW(tab, TCM_GETITEMRECT, tabIndex, reinterpret_cast(&tabRect)) 1099 | >> checkWin32Result("SendMessageW TCM_GETITEMRECT", successValue, true); 1100 | totaltabsWidth += rectWidth(tabRect); 1101 | 1102 | tabIndex += 1; 1103 | } 1104 | 1105 | auto tabRect = RECT{page.left, page.top, page.left + totaltabsWidth, page.top}; 1106 | SendMessageW(tab, TCM_ADJUSTRECT, true, reinterpret_cast(&tabRect)); 1107 | SetWindowPos(tab, nullptr, tabRect.left, tabRect.top, rectWidth(tabRect), rectHeight(tabRect), SWP_NOZORDER) 1108 | >> checkWin32Result("SetWindowPos", errorValue, false); 1109 | 1110 | auto buttonOffset = buttonPadding * 2; 1111 | for(auto id : bottomButtons) { 1112 | createControl(dialogBox, id, WC_BUTTONW, getText(languageData, id).c_str(), 1113 | WS_VISIBLE, 0, buttonOffset, page.bottom + buttonPadding, buttonWidth, buttonHeight).release(); 1114 | 1115 | buttonOffset += buttonWidth + buttonPadding; 1116 | } 1117 | 1118 | auto replayListRect = page; 1119 | replayListRect.bottom = page.bottom - replayDescriptionHeight; 1120 | createListView(dialogBox, replayList, replayListColumns, replayListRect, languageData).release(); 1121 | 1122 | createControl(dialogBox, replayDescription, WC_EDITW, {}, 1123 | WS_VSCROLL|ES_MULTILINE|ES_READONLY, WS_EX_CLIENTEDGE, 1124 | page.left, page.bottom - replayDescriptionHeight, rectWidth(page), replayDescriptionHeight).release(); 1125 | 1126 | createControl(dialogBox, replayFolder, WC_BUTTONW, getText(languageData, replayFolder).c_str(), 1127 | 0, 0, page.right - 1 * (buttonPadding + buttonWidth), page.bottom + buttonPadding, buttonWidth, buttonHeight).release(); 1128 | createControl(dialogBox, fixReplay, WC_BUTTONW, getText(languageData, fixReplay).c_str(), 1129 | 0, 0, page.right - 2 * (buttonPadding + buttonWidth), page.bottom + buttonPadding, buttonWidth, buttonHeight).release(); 1130 | 1131 | createListView(dialogBox, modList, modListColumns, page, languageData).release(); 1132 | 1133 | createControl(dialogBox, modFolder, WC_BUTTONW, getText(languageData, modFolder).c_str(), 1134 | 0, 0, page.right - 1 * (buttonPadding + buttonWidth), page.bottom + buttonPadding, buttonWidth, buttonHeight).release(); 1135 | 1136 | for(auto id : {gameBrowserLaunchGame, gameBrowserCancel, replayDescription, fixReplay, replayFolder, modFolder}) { 1137 | SendMessageW(getControlByID(dialogBox, id), WM_SETFONT, reinterpret_cast(font.get()), true); 1138 | } 1139 | 1140 | //When initialization finishes, set mod tab as the default tab 1141 | auto modPosition = std::find(std::begin(tabs), std::end(tabs), mods) - std::begin(tabs); 1142 | SendMessageW(tab, TCM_SETCURFOCUS, modPosition, 0); 1143 | 1144 | return TRUE; 1145 | }; 1146 | 1147 | handlers[WM_PAINT] = [&banner640Brush](HWND dialogBox, WPARAM, LPARAM lParam) { 1148 | if(banner640Brush.get() == nullptr) { 1149 | return FALSE; 1150 | } 1151 | auto painter = beginPaint(dialogBox); 1152 | FillRect(painter->context, &bannerRect, banner640Brush.get()) >> checkWin32Result("FillRect", errorValue, false); 1153 | return TRUE; 1154 | }; 1155 | 1156 | auto replaysAndMods = ReplaysAndModsData{}; 1157 | auto launchOptions = std::optional {}; 1158 | 1159 | auto getCurrentTabID = [](HWND dialogBox) { 1160 | auto selected = SendMessageW(getControlByID(dialogBox, gameBrowserTabs), TCM_GETCURSEL, 0, 0) 1161 | >> checkWin32Result("SendMessageW TCM_GETCURSEL", errorValue, -1); 1162 | if(static_cast(selected) >= tabs.size()) { 1163 | throw std::out_of_range("currentSelection > tabs.size()"); 1164 | } 1165 | return std::begin(tabs)[selected]; 1166 | }; 1167 | 1168 | auto updateListWindow = [&replaysAndMods](HWND dialogBox, ID id) { 1169 | auto window = HWND{nullptr}; 1170 | auto strings = std::vector> {}; 1171 | if(id == replays) { 1172 | window = getControlByID(dialogBox, replayList); 1173 | strings = replaysAndMods.replayDetailsToStrings(); 1174 | } 1175 | if(id == mods) { 1176 | window = getControlByID(dialogBox, modList); 1177 | strings = replaysAndMods.modDetailsToStrings(); 1178 | } 1179 | replaceListView(window, strings); 1180 | }; 1181 | 1182 | auto sortCurrentList = [&replaysAndMods, getCurrentTabID, updateListWindow](HWND dialogBox, std::size_t columnIndex) { 1183 | using namespace std::placeholders; 1184 | auto toggleSort = [](auto& detailsVector, auto memberAddress, auto compare) { 1185 | auto compareMember = std::bind(compare, std::bind(memberAddress, _1), std::bind(memberAddress, _2)); 1186 | if(std::is_sorted(std::begin(detailsVector), std::end(detailsVector), compareMember)) { 1187 | std::sort(std::rbegin(detailsVector), std::rend(detailsVector), compareMember); 1188 | } 1189 | else { 1190 | std::sort(std::begin(detailsVector), std::end(detailsVector), compareMember); 1191 | } 1192 | }; 1193 | 1194 | auto currentID = getCurrentTabID(dialogBox); 1195 | if(currentID == replays) { 1196 | if(columnIndex >= replayListColumns.size()) { throw std::out_of_range("columnIndex > replayListColumns.size()"); } 1197 | auto columnIs = [columnIndex](ID id) { return std::begin(replayListColumns)[columnIndex].first == id; }; 1198 | auto sortReplays = std::bind(toggleSort, std::ref(replaysAndMods.replayDetails), _1, _2); 1199 | using Details = ReplaysAndMods::ReplayDetails; 1200 | 1201 | if(columnIs(replayListReplayName)) sortReplays(&Details::replayName, StringLess{}); 1202 | if(columnIs(replayListModName)) sortReplays(&Details::modName, StringLess{}); 1203 | if(columnIs(replayListGameVersion)) sortReplays(&Details::gameVersion, std::less{}); 1204 | if(columnIs(replayListDate)) sortReplays(&Details::timeStamp, std::less{}); 1205 | } 1206 | 1207 | if(currentID == mods) { 1208 | if(columnIndex >= modListColumns.size()) { throw std::out_of_range("columnIndex > modListColumns.size()"); } 1209 | auto columnIs = [columnIndex](ID id) { return std::begin(modListColumns)[columnIndex].first == id; }; 1210 | auto sortModStrings = std::bind(toggleSort, std::ref(replaysAndMods.modDetails), _1, StringLess{}); 1211 | using Details = ReplaysAndMods::ModDetails; 1212 | 1213 | if(columnIs(modListModName)) sortModStrings(&Details::modName); 1214 | if(columnIs(modListModVersion)) sortModStrings(&Details::version); 1215 | } 1216 | 1217 | updateListWindow(dialogBox, currentID); 1218 | }; 1219 | 1220 | auto setSelected = [&languageData, &replaysAndMods, &launchOptions, getCurrentTabID](HWND dialogBox, std::size_t index) { 1221 | auto currentID = getCurrentTabID(dialogBox); 1222 | 1223 | if(currentID == replays and index < replaysAndMods.replayDetails.size()) { 1224 | const auto& replay = replaysAndMods.replayDetails[index]; 1225 | launchOptions = {LaunchOptions::replay, replay.fullPath}; 1226 | //update replay description 1227 | auto description = replaysAndMods.getReplayDescription(index, languageData); 1228 | SetWindowTextW(getControlByID(dialogBox, replayDescription), description.c_str()) 1229 | >> checkWin32Result("SetWindowTextW", successValue, true); 1230 | auto needFix = not replay.finalTimeCode.has_value(); 1231 | EnableWindow(getControlByID(dialogBox, fixReplay), needFix); 1232 | } 1233 | if(currentID == mods and index < replaysAndMods.modDetails.size()) { 1234 | const auto& mod = replaysAndMods.modDetails[index]; 1235 | launchOptions = {LaunchOptions::mod, mod.fullPath}; 1236 | } 1237 | EnableWindow(getControlByID(dialogBox, gameBrowserLaunchGame), true); 1238 | }; 1239 | 1240 | auto initializeTab = [&replaysAndMods, &launchOptions, getCurrentTabID, updateListWindow](HWND dialogBox) { 1241 | //disable launch game button 1242 | EnableWindow(getControlByID(dialogBox, gameBrowserLaunchGame), false); 1243 | EnableWindow(getControlByID(dialogBox, fixReplay), false); 1244 | launchOptions.reset(); 1245 | //clear replay detail box 1246 | SetWindowTextW(getControlByID(dialogBox, replayDescription), L"") 1247 | >> checkWin32Result("SetWindowTextW", successValue, true); 1248 | 1249 | auto currentID = getCurrentTabID(dialogBox); 1250 | 1251 | for(auto [tabID, windowList] : tabSubWindows) { 1252 | auto activeFlag = tabID == currentID; 1253 | for(auto windowID : windowList) { 1254 | auto window = getControlByID(dialogBox, windowID); 1255 | ShowWindow(window, activeFlag == true ? SW_SHOW : SW_HIDE); 1256 | EnableWindow(window, activeFlag and (windowID != fixReplay)); 1257 | } 1258 | } 1259 | 1260 | if(currentID == replays) { 1261 | replaysAndMods.replayDetails = ReplaysAndMods::getAllReplayDetails(); 1262 | } 1263 | if(currentID == mods) { 1264 | replaysAndMods.modDetails = ReplaysAndMods::getModSkudefs(); 1265 | } 1266 | 1267 | updateListWindow(dialogBox, currentID); 1268 | return TRUE; 1269 | }; 1270 | 1271 | auto fixReplayWorker = [&languageData, &launchOptions, initializeTab](HWND dialogBox) { 1272 | if(not launchOptions.has_value() or launchOptions->loadFileType != LaunchOptions::replay) { 1273 | return; 1274 | } 1275 | auto fix = MessageBoxW(dialogBox, 1276 | getText(languageData, fixReplayWarning).c_str(), 1277 | getText(languageData, fixReplay).c_str(), 1278 | MB_ICONWARNING|MB_YESNO) 1279 | >> checkWin32Result("MessageBoxW", errorValue, 0); 1280 | if(fix != IDYES) { 1281 | return; 1282 | } 1283 | try { 1284 | fixReplayByFileName(launchOptions->fileToBeLoaded); 1285 | notifyFixReplaySucceeded(languageData); 1286 | } 1287 | catch(const std::exception& error) { 1288 | notifyFixReplayFailed(error, languageData); 1289 | } 1290 | 1291 | initializeTab(dialogBox); 1292 | }; 1293 | 1294 | auto launchGame = [&launchOptions](HWND dialogBox) { 1295 | if(not launchOptions.has_value()) { 1296 | return; 1297 | } 1298 | EndDialog(dialogBox, 1) >> checkWin32Result("EndDialog", errorValue, false); 1299 | }; 1300 | 1301 | auto cancel = [](HWND dialogBox) { 1302 | EndDialog(dialogBox, 0) >> checkWin32Result("EndDialog", errorValue, false); 1303 | return TRUE; 1304 | }; 1305 | 1306 | handlers[WM_NOTIFY] = [sortCurrentList, setSelected, launchGame, initializeTab](HWND dialogBox, WPARAM, LPARAM dataHeaderAddress) { 1307 | auto dataHeader = reinterpret_cast(dataHeaderAddress); 1308 | 1309 | if(dataHeader->idFrom == gameBrowserTabs) { 1310 | if(dataHeader->code == TCN_SELCHANGE) { 1311 | initializeTab(dialogBox); 1312 | return TRUE; 1313 | } 1314 | } 1315 | 1316 | if(dataHeader->idFrom == replayList or dataHeader->idFrom == modList) { 1317 | if(dataHeader->code == LVN_COLUMNCLICK) { 1318 | auto listViewData = reinterpret_cast(dataHeader); 1319 | sortCurrentList(dialogBox, listViewData->iSubItem); 1320 | return TRUE; 1321 | } 1322 | if(dataHeader->code == LVN_ITEMCHANGED) { 1323 | auto listViewData = reinterpret_cast(dataHeader); 1324 | if(listViewData->uNewState bitand LVIS_FOCUSED) { 1325 | setSelected(dialogBox, listViewData->iItem); 1326 | return TRUE; 1327 | } 1328 | return TRUE; 1329 | } 1330 | if(dataHeader->code == NM_DBLCLK) { 1331 | if(dataHeader->idFrom == replayList or dataHeader->idFrom == modList) { 1332 | launchGame(dialogBox); 1333 | } 1334 | return TRUE; 1335 | } 1336 | } 1337 | return FALSE; 1338 | }; 1339 | 1340 | handlers[WM_COMMAND] = [fixReplayWorker, launchGame, cancel](HWND dialogBox, WPARAM codeAndIdentifier, LPARAM controlHandle) { 1341 | auto notificationCode = HIWORD(codeAndIdentifier); 1342 | auto identifier = LOWORD(codeAndIdentifier); 1343 | if(notificationCode == BN_CLICKED) { 1344 | if(identifier == gameBrowserLaunchGame) { 1345 | launchGame(dialogBox); 1346 | return TRUE; 1347 | } 1348 | if(identifier == gameBrowserCancel) { 1349 | cancel(dialogBox); 1350 | return TRUE; 1351 | } 1352 | 1353 | 1354 | if(identifier == replayFolder) { 1355 | shellExecute(dialogBox, L"explore", ReplaysAndMods::concatenateWithReplayFolder({}).c_str()); 1356 | return TRUE; 1357 | } 1358 | if(identifier == fixReplay) { 1359 | fixReplayWorker(dialogBox); 1360 | return TRUE; 1361 | } 1362 | if(identifier == modFolder) { 1363 | shellExecute(dialogBox, L"explore", ReplaysAndMods::concatenateWithModRootFolder({}).c_str()); 1364 | return TRUE; 1365 | } 1366 | } 1367 | return FALSE; 1368 | }; 1369 | 1370 | handlers[WM_CLOSE] = std::bind(cancel, std::placeholders::_1); 1371 | 1372 | auto launch = (modalDialogBox(handlers, WS_VISIBLE|WS_SYSMENU, 0, controlCenter) == 1); 1373 | 1374 | if(launch == false) { 1375 | launchOptions.reset(); 1376 | } 1377 | 1378 | return launchOptions; 1379 | } 1380 | 1381 | void aboutWindow(HWND parent, HICON icon, const LanguageData& languageData) { 1382 | 1383 | static constexpr auto clientArea = RECT{0, 0, 330, 420}; 1384 | static constexpr auto logoRect = RECT{15, 310, 315, 410}; 1385 | auto handlers = ModalDialogBox::HandlerTable{}; 1386 | auto font = getNormalFont(); 1387 | auto ra3barData = loadBinaryDataResource(GetModuleHandle(nullptr), MAKEINTRESOURCEW(RA3BAR_LOGO), RT_RCDATA); 1388 | auto ra3barBitmap = BitmapHandle{}; 1389 | auto maskBitmap = BitmapHandle{}; 1390 | auto initializeLogo = [&ra3barData, &ra3barBitmap, &maskBitmap](HDC context, int width, int height) { 1391 | ra3barBitmap = createCompatibleBitmap(context, width, height); 1392 | { 1393 | auto imageBits = getBitmapBits(context, ra3barBitmap.get(), {{width, -height}}); 1394 | auto i = std::size_t{0}; 1395 | for(auto color : ra3barData) { 1396 | auto alpha = (color >> 24) bitand 0xFF; 1397 | auto red = (color >> 16) bitand 0xFF; 1398 | auto green = (color >> 8) bitand 0xFF; 1399 | auto blue = (color >> 0) bitand 0xFF; 1400 | imageBits.buffer.at(i) = RGB(red, green, blue) * (alpha != 0); 1401 | ++i; 1402 | } 1403 | setBitmapBits(context, ra3barBitmap.get(), imageBits); 1404 | } 1405 | 1406 | maskBitmap = createCompatibleBitmap(context, width, height); 1407 | { 1408 | constexpr auto white = DWORD{0x00FFFFFF}; 1409 | auto maskBits = getBitmapBits(context, maskBitmap.get(), {{width, -height}}); 1410 | auto i = std::size_t{0}; 1411 | for(auto color : ra3barData) { 1412 | maskBits.buffer.at(i) = white * ((color >> 24) == 0); 1413 | ++i; 1414 | } 1415 | setBitmapBits(context, maskBitmap.get(), maskBits); 1416 | } 1417 | }; 1418 | 1419 | handlers[WM_INITDIALOG] = [parent, icon, &languageData, &font, initializeLogo](HWND dialogBox, WPARAM, LPARAM) { 1420 | myBeginDialog(dialogBox, getText(languageData, about), icon, 1421 | parent, ICC_STANDARD_CLASSES|ICC_LINK_CLASS, clientArea); 1422 | auto text = getText(languageData, aboutText) + L"\r\n\r\n" + getText(languageData, resourceAuthors); 1423 | auto control = createControl(dialogBox, aboutText, WC_LINK, text.c_str(), WS_VISIBLE, 0, 10, 10, 310, 300).release(); 1424 | SendMessageW(control, WM_SETFONT, reinterpret_cast(font.get()), true); 1425 | initializeLogo(getDeviceContext(dialogBox)->context, 300, 100); 1426 | return TRUE; 1427 | }; 1428 | handlers[WM_PAINT] = [&ra3barBitmap, &maskBitmap](HWND dialogBox, WPARAM, LPARAM) { 1429 | auto paint = beginPaint(dialogBox); 1430 | 1431 | auto mask = createCompatibleDeviceContext(paint->context); 1432 | auto previousMaskBitmap = selectObject(mask.get(), maskBitmap.get()); 1433 | auto [x, y, ex, ey] = logoRect; 1434 | BitBlt(paint->context, x, y, ex - x, ey - y, mask.get(), 0, 0, SRCAND) 1435 | >> checkWin32Result("BitBlt", errorValue, false); 1436 | 1437 | auto logo = createCompatibleDeviceContext(paint->context); 1438 | auto previousLogoBitmap = selectObject(logo.get(), ra3barBitmap.get()); 1439 | BitBlt(paint->context, x, y, ex - x, ey - y, logo.get(), 0, 0, SRCPAINT) 1440 | >> checkWin32Result("BitBlt", errorValue, false); 1441 | return TRUE; 1442 | }; 1443 | handlers[WM_NOTIFY] = [](HWND dialogBox, WPARAM, LPARAM dataHeaderAddress) { 1444 | const auto& dataHeader = *reinterpret_cast(dataHeaderAddress); 1445 | if(dataHeader.idFrom == aboutText and dataHeader.code == NM_CLICK) { 1446 | const auto& linkInformation = *reinterpret_cast(dataHeaderAddress); 1447 | shellExecute(dialogBox, L"open", linkInformation.item.szUrl); 1448 | return TRUE; 1449 | } 1450 | return FALSE; 1451 | }; 1452 | handlers[WM_CLOSE] = [](HWND dialogBox, WPARAM, LPARAM) { 1453 | EndDialog(dialogBox, 0) >> checkWin32Result("EndDialog", errorValue, false); 1454 | return TRUE; 1455 | }; 1456 | modalDialogBox(handlers, WS_VISIBLE|WS_SYSMENU, 0, parent); 1457 | } 1458 | --------------------------------------------------------------------------------