diff --git a/Core/GameEngine/Include/Common/UserPreferences.h b/Core/GameEngine/Include/Common/UserPreferences.h index 541303f6866..f1b9f3ea786 100644 --- a/Core/GameEngine/Include/Common/UserPreferences.h +++ b/Core/GameEngine/Include/Common/UserPreferences.h @@ -68,6 +68,7 @@ class UserPreferences : public PreferenceMap protected: AsciiString m_filename; + Bool m_loadFailed; ///< the file exists but could not be read; write() must not replace it }; //----------------------------------------------------------------------------- diff --git a/Core/GameEngine/Include/GameClient/MapUtil.h b/Core/GameEngine/Include/GameClient/MapUtil.h index 50ce0d6073c..c8b2bec0873 100644 --- a/Core/GameEngine/Include/GameClient/MapUtil.h +++ b/Core/GameEngine/Include/GameClient/MapUtil.h @@ -118,7 +118,7 @@ class MapCache : public std::map private: void prepareUnseenMaps(const AsciiString &mapDir); Bool clearUnseenMaps(const AsciiString &mapDir); - void loadMapsFromMapCacheINI(const AsciiString &mapDir); + Bool loadMapsFromMapCacheINI(const AsciiString &mapDir); // returns false if the cache file was unreadable Bool loadMapsFromDisk(const AsciiString &mapDir, Bool isOfficial, Bool filterByAllowedMaps = FALSE); // returns true if we needed to (re)parse a map Bool addMap(const AsciiString &mapDir, const AsciiString &fname, const AsciiString &lowerFname, FileInfo &fileInfo, Bool isOfficial); ///< returns true if it had to (re)parse the map void writeCacheINI(const AsciiString &mapDir); diff --git a/Core/GameEngine/Source/Common/INI/INI.cpp b/Core/GameEngine/Source/Common/INI/INI.cpp index 2c73a39872e..c03028c6d4c 100644 --- a/Core/GameEngine/Source/Common/INI/INI.cpp +++ b/Core/GameEngine/Source/Common/INI/INI.cpp @@ -297,6 +297,12 @@ void INI::prepFile( AsciiString filename, INILoadType loadType ) m_readBufferNext = 0; m_readBufferUsed = file->size(); m_readBuffer = file->readEntireAndClose(); + if (m_readBuffer == nullptr) + { + m_readBufferUsed = 0; + DEBUG_CRASH(( "INI::load, cannot read file '%s'", filename.str() )); + throw INI_CANT_OPEN_FILE; + } // save our filename m_filename = filename; diff --git a/Core/GameEngine/Source/Common/System/AsciiString.cpp b/Core/GameEngine/Source/Common/System/AsciiString.cpp index 233272abf0c..afa1b96186b 100644 --- a/Core/GameEngine/Source/Common/System/AsciiString.cpp +++ b/Core/GameEngine/Source/Common/System/AsciiString.cpp @@ -46,6 +46,9 @@ #include "Common/CriticalSection.h" +#if defined(_WIN32) && !defined(WC_NO_BEST_FIT_CHARS) +#define WC_NO_BEST_FIT_CHARS 0x00000400 +#endif // ----------------------------------------------------- @@ -307,9 +310,43 @@ char* AsciiString::getBufferForRead(Int len) void AsciiString::translate(const UnicodeString& stringSrc) { validate(); - /// @todo srj put in a real translation here; this will only work for 7-bit ascii - clear(); Int len = stringSrc.getLength(); +#ifdef _WIN32 + // non-ASCII text becomes process code page bytes (UTF-8 on modern Windows); pure ASCII takes the loop below + Bool isAscii = true; + for (Int i = 0; i < len; i++) + { + if (stringSrc.getCharAt(i) >= 0x80) + { + isAscii = false; + break; + } + } + if (!isAscii) + { + // a legacy code page can't hold everything; substitute '_' so generated file names stay valid + const Bool isUtf8 = GetACP() == CP_UTF8; + const UINT codePage = isUtf8 ? CP_UTF8 : CP_ACP; + const DWORD flags = isUtf8 ? 0 : WC_NO_BEST_FIT_CHARS; + const char* defaultChar = isUtf8 ? nullptr : "_"; + const Int narrowLen = WideCharToMultiByte(codePage, flags, stringSrc.str(), len, nullptr, 0, defaultChar, nullptr); + if (narrowLen > 0) + { + char* narrowBuf = new char[narrowLen + 1]; + const Int written = WideCharToMultiByte(codePage, flags, stringSrc.str(), len, narrowBuf, narrowLen, defaultChar, nullptr); + if (written > 0) + { + narrowBuf[written] = 0; + set(narrowBuf); + delete[] narrowBuf; + validate(); + return; + } + delete[] narrowBuf; + } + } +#endif + clear(); for (Int i = 0; i < len; i++) concat((char)stringSrc.getCharAt(i)); validate(); diff --git a/Core/GameEngine/Source/Common/System/LocalFile.cpp b/Core/GameEngine/Source/Common/System/LocalFile.cpp index 431906a8ddd..3ec759fcc80 100644 --- a/Core/GameEngine/Source/Common/System/LocalFile.cpp +++ b/Core/GameEngine/Source/Common/System/LocalFile.cpp @@ -731,9 +731,16 @@ char* LocalFile::readEntireAndClose() UnsignedInt fileSize = size(); char* buffer = NEW char[fileSize]; - read(buffer, fileSize); + const Int bytesRead = read(buffer, fileSize); close(); + // a short read leaves uninitialized bytes in the buffer, which callers would parse as file content + if (bytesRead < 0 || (UnsignedInt)bytesRead != fileSize) + { + delete[] buffer; + return nullptr; + } + return buffer; } diff --git a/Core/GameEngine/Source/Common/System/UnicodeString.cpp b/Core/GameEngine/Source/Common/System/UnicodeString.cpp index 386778d321b..e21c0ea38dc 100644 --- a/Core/GameEngine/Source/Common/System/UnicodeString.cpp +++ b/Core/GameEngine/Source/Common/System/UnicodeString.cpp @@ -221,9 +221,38 @@ WideChar* UnicodeString::getBufferForRead(Int len) void UnicodeString::translate(const AsciiString& stringSrc) { validate(); - /// @todo srj put in a real translation here; this will only work for 7-bit ascii - clear(); Int len = stringSrc.getLength(); +#ifdef _WIN32 + // bytes >= 0x80 are process code page text (UTF-8 on modern Windows); pure ASCII takes the loop below + Bool isAscii = true; + for (Int i = 0; i < len; i++) + { + if ((unsigned char)stringSrc.getCharAt(i) >= 0x80) + { + isAscii = false; + break; + } + } + if (!isAscii) + { + const Int wideLen = MultiByteToWideChar(CP_ACP, 0, stringSrc.str(), len, nullptr, 0); + if (wideLen > 0) + { + WideChar* wideBuf = new WideChar[wideLen + 1]; + const Int written = MultiByteToWideChar(CP_ACP, 0, stringSrc.str(), len, wideBuf, wideLen); + if (written > 0) + { + wideBuf[written] = 0; + set(wideBuf); + delete[] wideBuf; + validate(); + return; + } + delete[] wideBuf; + } + } +#endif + clear(); for (Int i = 0; i < len; i++) concat((WideChar)stringSrc.getCharAt(i)); validate(); diff --git a/Core/GameEngine/Source/Common/UserPreferences.cpp b/Core/GameEngine/Source/Common/UserPreferences.cpp index 844b511922b..ae9f7636b7b 100644 --- a/Core/GameEngine/Source/Common/UserPreferences.cpp +++ b/Core/GameEngine/Source/Common/UserPreferences.cpp @@ -110,7 +110,7 @@ static AsciiString realAsStr(Real val) // UserPreferences Class //----------------------------------------------------------------------------- -UserPreferences::UserPreferences() +UserPreferences::UserPreferences() : m_loadFailed(FALSE) { } @@ -126,8 +126,14 @@ Bool UserPreferences::load(AsciiString fname) m_filename = TheGlobalData->getPath_UserData(); m_filename.concat(fname); + m_loadFailed = FALSE; FILE *fp = fopen(m_filename.str(), "r"); + if (fp == nullptr && errno != ENOENT) + { + DEBUG_LOG(("UserPreferences::load - could not open '%s', errno %d", m_filename.str(), errno)); + m_loadFailed = TRUE; + } if (fp) { char buf[LINE_LEN]; @@ -138,6 +144,8 @@ Bool UserPreferences::load(AsciiString fname) AsciiString key, val; line.nextToken(&key, "="); + if (line.isEmpty()) // no '=' in the line, nothing follows the key + continue; val = line.str() + 1; key.trim(); @@ -148,7 +156,14 @@ Bool UserPreferences::load(AsciiString fname) (*this)[key] = val; } + const Bool readFailed = ferror(fp) != 0; fclose(fp); + if (readFailed) + { + DEBUG_LOG(("UserPreferences::load - read error in '%s'", m_filename.str())); + m_loadFailed = TRUE; + return false; + } return true; } return false; @@ -159,17 +174,43 @@ Bool UserPreferences::write() if (m_filename.isEmpty()) return false; - FILE *fp = fopen(m_filename.str(), "w"); + // the existing file is unreadable, replacing it with what we hold would lose the user's settings + if (m_loadFailed) + return false; + + // write to a temp file and swap it in so a failed write never leaves a truncated preference file + AsciiString tempFilename = m_filename; + tempFilename.concat(".tmp"); + + FILE *fp = fopen(tempFilename.str(), "w"); if (fp) { + Bool ok = TRUE; PreferenceMap::const_iterator it = begin(); while (it != end()) { - fprintf(fp, "%s = %s\n", it->first.str(), it->second.str()); + if (fprintf(fp, "%s = %s\n", it->first.str(), it->second.str()) < 0) + ok = FALSE; ++it; } - fclose(fp); - return true; + if (fflush(fp) != 0 || ferror(fp)) + ok = FALSE; + if (fclose(fp) != 0) + ok = FALSE; + +#ifdef _WIN32 + if (ok && !MoveFileExA(tempFilename.str(), m_filename.str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) + ok = FALSE; +#else + if (ok && rename(tempFilename.str(), m_filename.str()) != 0) + ok = FALSE; +#endif + if (!ok) + { + DEBUG_LOG(("UserPreferences::write - failed to write '%s'", m_filename.str())); + remove(tempFilename.str()); + } + return ok; } return false; } diff --git a/Core/GameEngine/Source/GameClient/GUI/IMEManager.cpp b/Core/GameEngine/Source/GameClient/GUI/IMEManager.cpp index 5de6d1a5c54..74cdec5f02e 100644 --- a/Core/GameEngine/Source/GameClient/GUI/IMEManager.cpp +++ b/Core/GameEngine/Source/GameClient/GUI/IMEManager.cpp @@ -673,14 +673,15 @@ Bool IMEManager::serviceIMEMessage( void *windowsHandle, UnsignedInt message, In // -------------------------------------------------------------------- case WM_IME_CHAR: { - WideChar wchar = convertCharToWide(wParam); + // Unicode windows get UTF-16, ANSI windows get the character in the ANSI code page + WideChar wchar = IsWindowUnicode( (HWND)windowsHandle ) ? (WideChar) (wParam & 0xffff) : convertCharToWide(wParam); #ifdef DEBUG_IME DEBUG_LOG(("IMM: WM_IME_CHAR - '%hc'0x%04x", wchar, wchar )); #endif if ( m_window && (wchar > 32 || wchar == VK_RETURN )) { - TheWindowManager->winSendInputMsg( m_window, GWM_IME_CHAR, (wParam & 0xffff), lParam ); + TheWindowManager->winSendInputMsg( m_window, GWM_IME_CHAR, wchar, lParam ); m_result = 0; return TRUE; } diff --git a/Core/GameEngine/Source/GameClient/MapUtil.cpp b/Core/GameEngine/Source/GameClient/MapUtil.cpp index 28814458683..ef776c2e1f7 100644 --- a/Core/GameEngine/Source/GameClient/MapUtil.cpp +++ b/Core/GameEngine/Source/GameClient/MapUtil.cpp @@ -342,8 +342,12 @@ void MapCache::writeCacheINI( const AsciiString &mapDir ) TheFileSystem->createDirectory(mapDir); filepath.concat(m_mapCacheName); - FILE *fp = fopen(filepath.str(), "w"); - DEBUG_ASSERTCRASH(fp != nullptr, ("Failed to create %s", filepath.str())); + + // write to a temp file and swap it in so a failed write never leaves a truncated MapCache.ini + AsciiString tempFilepath = filepath; + tempFilepath.concat(".tmp"); + FILE *fp = fopen(tempFilepath.str(), "w"); + DEBUG_ASSERTCRASH(fp != nullptr, ("Failed to create %s", tempFilepath.str())); if (fp == nullptr) { return; } @@ -406,7 +410,22 @@ void MapCache::writeCacheINI( const AsciiString &mapDir ) } } - fclose(fp); + Bool ok = fflush(fp) == 0 && !ferror(fp); + if (fclose(fp) != 0) + ok = FALSE; + +#ifdef _WIN32 + if (ok && !MoveFileExA(tempFilepath.str(), filepath.str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) + ok = FALSE; +#else + if (ok && rename(tempFilepath.str(), filepath.str()) != 0) + ok = FALSE; +#endif + if (!ok) + { + DEBUG_LOG(("MapCache::writeCacheINI - failed to write '%s'", filepath.str())); + remove(tempFilepath.str()); + } } void MapCache::updateCache() @@ -501,7 +520,7 @@ Bool MapCache::clearUnseenMaps( const AsciiString &mapDir ) return erasedSomething; } -void MapCache::loadMapsFromMapCacheINI( const AsciiString &mapDir ) +Bool MapCache::loadMapsFromMapCacheINI( const AsciiString &mapDir ) { INI ini; AsciiString fname; @@ -509,8 +528,18 @@ void MapCache::loadMapsFromMapCacheINI( const AsciiString &mapDir ) if (TheFileSystem->doesFileExist(fname.str())) { - ini.load( fname, INI_LOAD_OVERWRITE, nullptr ); + // a partial or unreadable cache must not abort startup, the disk scan re-caches the maps and rewrites it + try + { + ini.load( fname, INI_LOAD_OVERWRITE, nullptr ); + } + catch (...) + { + DEBUG_LOG(("MapCache::loadMapsFromMapCacheINI - ignoring unreadable '%s'", fname.str())); + return FALSE; + } } + return TRUE; } Bool MapCache::loadMapsFromDisk( const AsciiString &mapDir, Bool isOfficial, Bool filterByAllowedMaps ) @@ -634,7 +663,25 @@ Bool MapCache::addMap( DEBUG_LOG(("MapCache::addMap(): caching '%s' because '%s' was not found", fname.str(), lowerFname.str())); - loadMap(fname); // Just load for querying the data, since we aren't playing this map. + // Just load for querying the data, since we aren't playing this map. + Bool loaded = FALSE; + try + { + loaded = loadMap(fname); + } + catch (...) + { + loaded = FALSE; + } + + const UnsignedInt mapCRC = loaded ? calcCRC(fname) : 0; + if (!loaded || mapCRC == 0) + { + // never cache a map we could not read; drop a stale entry so writeCacheINI does not persist it + DEBUG_LOG(("MapCache::addMap(): could not read '%s', not caching it", fname.str())); + resetMap(); + return erase(lowerFname) > 0; + } // The map is now loaded. Pick out what we need. MapMetaData md; @@ -649,7 +696,7 @@ Bool MapCache::addMap( md.m_timestamp.m_lowTimeStamp = fileInfo.timestampLow; md.m_supplyPositions = m_supplyPositions; md.m_techPositions = m_techPositions; - md.m_CRC = calcCRC(fname); + md.m_CRC = mapCRC; Bool exists = false; AsciiString nameLookupTag = worldDict.getAsciiString(TheKey_mapName, &exists); diff --git a/Core/GameEngine/Source/GameClient/System/Image.cpp b/Core/GameEngine/Source/GameClient/System/Image.cpp index 847259e28e3..bf41ee17d8c 100644 --- a/Core/GameEngine/Source/GameClient/System/Image.cpp +++ b/Core/GameEngine/Source/GameClient/System/Image.cpp @@ -253,8 +253,10 @@ void ImageCollection::load( Int textureSize ) if(TheGlobalData) { userDataPath.format("%sINI\\MappedImages\\*.ini",TheGlobalData->getPath_UserData().str()); - if(FindFirstFile(userDataPath.str(), &findData) !=INVALID_HANDLE_VALUE) + HANDLE hFind = FindFirstFile(userDataPath.str(), &findData); + if(hFind != INVALID_HANDLE_VALUE) { + FindClose(hFind); userDataPath.format("%sINI\\MappedImages",TheGlobalData->getPath_UserData().str()); ini.loadDirectory(userDataPath, INI_LOAD_OVERWRITE, nullptr ); } diff --git a/Core/GameEngine/Source/GameNetwork/ConnectionManager.cpp b/Core/GameEngine/Source/GameNetwork/ConnectionManager.cpp index 20454323318..f347db5a83f 100644 --- a/Core/GameEngine/Source/GameNetwork/ConnectionManager.cpp +++ b/Core/GameEngine/Source/GameNetwork/ConnectionManager.cpp @@ -809,7 +809,9 @@ void ConnectionManager::processFile(NetFileCommandMsg *msg) { #ifdef DEBUG_LOGGING UnicodeString log; - log.format(L"Saw file transfer: '%hs' of %d bytes from %d", msg->getPortableFilename().str(), msg->getFileLength(), msg->getPlayerID()); + UnicodeString uFilename; + uFilename.translate(msg->getPortableFilename()); + log.format(L"Saw file transfer: '%s' of %d bytes from %d", uFilename.str(), msg->getFileLength(), msg->getPlayerID()); DEBUG_LOG(("%ls", log.str())); #endif @@ -2515,7 +2517,9 @@ UnsignedShort ConnectionManager::sendFileAnnounce(AsciiString path, UnsignedByte if (!theFile || !theFile->size()) { UnicodeString log; - log.format(L"Not sending file '%hs' to %X", path.str(), playerMask); + UnicodeString uPath; + uPath.translate(path); + log.format(L"Not sending file '%s' to %X", uPath.str(), playerMask); DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("%ls", log.str())); if (TheLAN) TheLAN->OnChat(L"sendFile", 0, log, LANAPI::LANCHAT_SYSTEM); @@ -2553,7 +2557,9 @@ void ConnectionManager::sendFile(AsciiString path, UnsignedByte playerMask, Unsi if (!theFile || !theFile->size()) { UnicodeString log; - log.format(L"Not sending file '%hs' to %X", path.str(), playerMask); + UnicodeString uPath; + uPath.translate(path); + log.format(L"Not sending file '%s' to %X", uPath.str(), playerMask); DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("%ls", log.str())); if (TheLAN) TheLAN->OnChat(L"sendFile", 0, log, LANAPI::LANCHAT_SYSTEM); @@ -2562,6 +2568,11 @@ void ConnectionManager::sendFile(AsciiString path, UnsignedByte playerMask, Unsi Int len = theFile->size(); char *buf = theFile->readEntireAndClose(); + if (buf == nullptr) + { + DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("Not sending file '%s' to %X, it could not be read", path.str(), playerMask)); + return; + } NetCommandDataChunk rawDataChunk(buf, len); // compress Targas diff --git a/Core/GameEngine/Source/GameNetwork/LANAPI.cpp b/Core/GameEngine/Source/GameNetwork/LANAPI.cpp index 5d8914ffcd5..ef244dd8675 100644 --- a/Core/GameEngine/Source/GameNetwork/LANAPI.cpp +++ b/Core/GameEngine/Source/GameNetwork/LANAPI.cpp @@ -769,7 +769,7 @@ void LANAPI::RequestHasMap() } else { - mapDisplayName.format(L"%hs", TheGameState->getMapLeafName(m_currentGame->getMap()).str()); + mapDisplayName.translate(TheGameState->getMapLeafName(m_currentGame->getMap()).str()); willTransfer = WouldMapTransfer(m_currentGame->getMap()); } if (willTransfer) diff --git a/Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp b/Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp index e99c5c8c25c..701ef3e987e 100644 --- a/Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp +++ b/Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp @@ -155,7 +155,7 @@ void LANAPI::OnHasMap( UnsignedInt playerIP, Bool status ) } else { - mapDisplayName.format(L"%hs", m_currentGame->getMap().str()); + mapDisplayName.translate(m_currentGame->getMap().str()); willTransfer = WouldMapTransfer(m_currentGame->getMap()); } if (!status) diff --git a/Core/GameEngineDevice/Source/MilesAudioDevice/MilesAudioManager.cpp b/Core/GameEngineDevice/Source/MilesAudioDevice/MilesAudioManager.cpp index e1cd3e9dbec..c6b2f6e9198 100644 --- a/Core/GameEngineDevice/Source/MilesAudioDevice/MilesAudioManager.cpp +++ b/Core/GameEngineDevice/Source/MilesAudioDevice/MilesAudioManager.cpp @@ -3118,6 +3118,9 @@ void *AudioFileCache::openFile( AudioEventRTS *eventToOpenFrom ) UnsignedInt fileSize = file->size(); char* buffer = file->readEntireAndClose(); + if (buffer == nullptr) { + return nullptr; + } OpenAudioFile openedAudioFile; openedAudioFile.m_eventInfo = eventToOpenFrom->getAudioEventInfo(); diff --git a/Core/GameEngineDevice/Source/Win32Device/Common/Win32BIGFileSystem.cpp b/Core/GameEngineDevice/Source/Win32Device/Common/Win32BIGFileSystem.cpp index 64401c47eff..b824dcaed57 100644 --- a/Core/GameEngineDevice/Source/Win32Device/Common/Win32BIGFileSystem.cpp +++ b/Core/GameEngineDevice/Source/Win32Device/Common/Win32BIGFileSystem.cpp @@ -96,9 +96,9 @@ ArchiveFile * Win32BIGFileSystem::openArchiveFile(const Char *filename) { AsciiString asciibuf; char buffer[_MAX_PATH]; - fp->read(buffer, 4); // read the "BIG" at the beginning of the file. + // read the "BIG" at the beginning of the file. buffer[4] = 0; - if (strcmp(buffer, BIGFileIdentifier) != 0) { + if (fp->read(buffer, 4) != 4 || strcmp(buffer, BIGFileIdentifier) != 0) { DEBUG_CRASH(("Error reading BIG file identifier in file %s", filename)); fp->close(); fp = nullptr; @@ -106,7 +106,12 @@ ArchiveFile * Win32BIGFileSystem::openArchiveFile(const Char *filename) { } // read in the file size. - fp->read(&archiveFileSize, 4); + if (fp->read(&archiveFileSize, 4) != 4) { + DEBUG_CRASH(("Error reading BIG file size in file %s", filename)); + fp->close(); + fp = nullptr; + return nullptr; + } DEBUG_LOG(("Win32BIGFileSystem::openArchiveFile - size of archive file is %d bytes", archiveFileSize)); @@ -114,7 +119,12 @@ ArchiveFile * Win32BIGFileSystem::openArchiveFile(const Char *filename) { // read in the number of files contained in this BIG file. // change the order of the bytes cause the file size is in reverse byte order for some reason. - fp->read(&numLittleFiles, 4); + if (fp->read(&numLittleFiles, 4) != 4) { + DEBUG_CRASH(("Error reading BIG file count in file %s", filename)); + fp->close(); + fp = nullptr; + return nullptr; + } numLittleFiles = betoh(numLittleFiles); DEBUG_LOG(("Win32BIGFileSystem::openArchiveFile - %d are contained in archive", numLittleFiles)); @@ -131,11 +141,14 @@ ArchiveFile * Win32BIGFileSystem::openArchiveFile(const Char *filename) { // TheSuperHackers @fix Mauller 23/04/2025 Create new file handle when necessary to prevent memory leak ArchiveFile *archiveFile = NEW Win32BIGFile(filename, AsciiString::TheEmptyString); + Bool corrupt = FALSE; for (Int i = 0; i < numLittleFiles; ++i) { Int filesize = 0; Int fileOffset = 0; - fp->read(&fileOffset, 4); - fp->read(&filesize, 4); + if (fp->read(&fileOffset, 4) != 4 || fp->read(&filesize, 4) != 4) { + corrupt = TRUE; + break; + } filesize = betoh(filesize); fileOffset = betoh(fileOffset); @@ -145,11 +158,21 @@ ArchiveFile * Win32BIGFileSystem::openArchiveFile(const Char *filename) { fileInfo->m_size = filesize; // read in the path name of the file. + // the name must terminate within the buffer Int pathIndex = -1; - do { - ++pathIndex; - fp->read(buffer + pathIndex, 1); - } while (buffer[pathIndex] != 0); + Bool terminated = FALSE; + while (++pathIndex < (Int)sizeof(buffer)) { + if (fp->read(buffer + pathIndex, 1) != 1) + break; + if (buffer[pathIndex] == 0) { + terminated = TRUE; + break; + } + } + if (!terminated) { + corrupt = TRUE; + break; + } Int filenameIndex = pathIndex; while ((filenameIndex >= 0) && (buffer[filenameIndex] != '\\') && (buffer[filenameIndex] != '/')) { @@ -171,6 +194,15 @@ ArchiveFile * Win32BIGFileSystem::openArchiveFile(const Char *filename) { archiveFile->addFile(path, fileInfo); } + if (corrupt) { + DEBUG_CRASH(("Truncated or corrupt directory in BIG file %s", filename)); + delete archiveFile; + delete fileInfo; + fp->close(); + fp = nullptr; + return nullptr; + } + archiveFile->attachFile(fp); delete fileInfo; diff --git a/Core/Libraries/Source/Compression/CompressionManager.cpp b/Core/Libraries/Source/Compression/CompressionManager.cpp index 00e7d91ead7..2bb9cb0d443 100644 --- a/Core/Libraries/Source/Compression/CompressionManager.cpp +++ b/Core/Libraries/Source/Compression/CompressionManager.cpp @@ -390,6 +390,11 @@ void DoCompressTest() DEBUG_LOG(("***************************\nTesting '%s'\n", it->first.str())); Int origSize = f->size(); UnsignedByte *buf = (UnsignedByte *)f->readEntireAndClose(); + if (buf == nullptr) + { + ++it; + continue; + } UnsignedByte *uncompressedBuf = NEW UnsignedByte[origSize]; CompData d = s_sizes[it->first]; diff --git a/Generals/Code/GameEngine/Source/Common/System/DataChunk.cpp b/Generals/Code/GameEngine/Source/Common/System/DataChunk.cpp index 2b90ade8db2..fd83eea0eb5 100644 --- a/Generals/Code/GameEngine/Source/Common/System/DataChunk.cpp +++ b/Generals/Code/GameEngine/Source/Common/System/DataChunk.cpp @@ -57,6 +57,11 @@ Bool CachedFileInputStream::open(AsciiString path) if (m_size) { m_buffer = file->readEntireAndClose(); file = nullptr; + if (m_buffer == nullptr) + { + m_size = 0; + return FALSE; + } } m_pos=0; } @@ -246,12 +251,22 @@ DataChunkOutput::~DataChunkOutput() m_contents.write(*m_pOut); // Rewind the temp m_tmp_file + if (m_tmp_file == nullptr) + { + DEBUG_CRASH(("DataChunkOutput - temp file was never opened")); + return; + } ::fclose(m_tmp_file); AsciiString tmpFileName = TheGlobalData->getPath_UserData(); tmpFileName.concat(TEMP_FILENAME); m_tmp_file = ::fopen( tmpFileName.str(), "rb" ); + if (m_tmp_file == nullptr) + { + DEBUG_CRASH(("DataChunkOutput - could not reopen temp file '%s'", tmpFileName.str())); + return; + } ::fseek(m_tmp_file, 0, SEEK_SET); // append the temp m_tmp_file m_contents diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GameState.h b/GeneralsMD/Code/GameEngine/Include/Common/GameState.h index 16bc991b69a..cfc477c138b 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GameState.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GameState.h @@ -170,7 +170,7 @@ class GameState : public SubsystemInterface, // manipulating files Bool doesSaveGameExist( AsciiString filename ); ///< does the save file exist void populateSaveGameListbox( GameWindow *listbox, SaveLoadLayoutType layoutType ); ///< populate listbox with available save games - void getSaveGameInfoFromFile( AsciiString filename, SaveGameInfo *saveGameInfo ); ///< get save game info from file + Bool getSaveGameInfoFromFile( AsciiString filename, SaveGameInfo *saveGameInfo ); ///< get save game info from file, false if it can't be read void friend_xferSaveDataForCRC( Xfer *xfer, SnapshotType which ); ///< This should only be called to DeepCRC sanity checking diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/GeneralsOnline_Settings.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/GeneralsOnline_Settings.h index e9453892018..611bec0c9b4 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/GeneralsOnline_Settings.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/GeneralsOnline_Settings.h @@ -30,7 +30,7 @@ class GenOnlineSettings void Save_Camera_MaxHeight_WhenLobbyHost(float maxHeight) { - if (maxHeight >= GENERALS_ONLINE_MIN_LOBBY_CAMERA_ZOOM || maxHeight <= GENERALS_ONLINE_MAX_LOBBY_CAMERA_ZOOM) + if (maxHeight >= GENERALS_ONLINE_MIN_LOBBY_CAMERA_ZOOM && maxHeight <= GENERALS_ONLINE_MAX_LOBBY_CAMERA_ZOOM) { m_Camera_MaxHeight_LobbyHost = maxHeight; Save(); diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMP_include.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMP_include.h index 6bee19efb2e..342de5a1087 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMP_include.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMP_include.h @@ -27,6 +27,8 @@ void NetworkLog(ELogVerbosity logVerbosity, const char* fmt, ...); std::string to_utf8(const std::wstring& wstr); std::wstring from_utf8(const std::string& utf8_str); +std::string local_to_utf8(const std::string& local_str); +std::string utf8_to_local(const std::string& utf8_str); std::wstring NormalizeSingleLineText(const std::wstring& text); std::wstring WidenFormatSpecifiers(const std::wstring& format); diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.h index 98974b8391a..5e89ed8d313 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.h @@ -209,7 +209,7 @@ class NGMP_OnlineServices_LobbyInterface // updates - void UpdateCurrentLobby_Map(AsciiString strMap, AsciiString strMapPath, bool bIsOfficial, int newMaxPlayers); + void UpdateCurrentLobby_Map(UnicodeString strMap, AsciiString strMapPath, bool bIsOfficial, int newMaxPlayers); void UpdateCurrentLobby_LimitSuperweapons(bool bLimitSuperweapons); void UpdateCurrentLobby_StartingCash(UnsignedInt startingCashValue); diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp index 5a0d746d251..04063cf38f6 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp @@ -69,6 +69,49 @@ GlobalData* TheWritableGlobalData = nullptr; ///< The global data singleton //------------------------------------------------------------------------------------------------- GlobalData* GlobalData::m_theOriginal = nullptr; +#ifndef WC_NO_BEST_FIT_CHARS +#define WC_NO_BEST_FIT_CHARS 0x00000400 +#endif + +//------------------------------------------------------------------------------------------------- +// Converts to the process code page that every char* file API uses; fails instead of mangling characters it can't represent +static Bool wideToProcessCodePage(const wchar_t* widePath, AsciiString& out) +{ + const Bool isUtf8 = GetACP() == CP_UTF8; + const UINT codePage = isUtf8 ? CP_UTF8 : CP_ACP; + const DWORD flags = isUtf8 ? 0 : WC_NO_BEST_FIT_CHARS; + BOOL usedDefaultChar = FALSE; + BOOL* pUsedDefaultChar = isUtf8 ? nullptr : &usedDefaultChar; + + const int len = WideCharToMultiByte(codePage, flags, widePath, -1, nullptr, 0, nullptr, pUsedDefaultChar); + if (len <= 0 || usedDefaultChar) + return FALSE; + + char* buffer = new char[len]; + const int written = WideCharToMultiByte(codePage, flags, widePath, -1, buffer, len, nullptr, pUsedDefaultChar); + if (written > 0 && !usedDefaultChar) + out = buffer; + delete[] buffer; + return written > 0 && !usedDefaultChar; +} + +//------------------------------------------------------------------------------------------------- +// Narrow form of a wide path; falls back to the ASCII 8.3 short path when the code page can't hold it (pre UTF-8 Windows) +static AsciiString widePathToFileApiPath(const wchar_t* widePath) +{ + AsciiString path; + if (wideToProcessCodePage(widePath, path)) + return path; + + wchar_t shortPath[_MAX_PATH + 1]; + const DWORD shortLen = GetShortPathNameW(widePath, shortPath, _MAX_PATH + 1); + if (shortLen > 0 && shortLen <= _MAX_PATH && wideToProcessCodePage(shortPath, path)) + return path; + + DEBUG_LOG(("Documents path can't be represented in code page %u and has no short name", GetACP())); + return AsciiString::TheEmptyString; +} + /////////////////////////////////////////////////////////////////////////////////////////////////// // PRIVATE DATA /////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////// @@ -1075,10 +1118,13 @@ GlobalData::GlobalData() #else // Set user data directory based on registry settings instead of INI parameters. This allows us to // localize the leaf name. - char temp[_MAX_PATH + 1]; - if (::SHGetSpecialFolderPath(nullptr, temp, CSIDL_PERSONAL, true)) + wchar_t temp[_MAX_PATH + 1]; + AsciiString myDocumentsDirectory; + if (::SHGetSpecialFolderPathW(nullptr, temp, CSIDL_PERSONAL, true)) + myDocumentsDirectory = widePathToFileApiPath(temp); + + if (!myDocumentsDirectory.isEmpty()) { - AsciiString myDocumentsDirectory = temp; if (myDocumentsDirectory.getCharAt(myDocumentsDirectory.getLength() - 1) != '\\') myDocumentsDirectory.concat('\\'); @@ -1421,14 +1467,14 @@ AsciiString GlobalData::BuildUserDataPathFromRegistry() HRESULT hr = pSHGetKnownFolderPath(FOLDERID_Documents, KF_FLAG_DEFAULT, nullptr, &pszPath); if (SUCCEEDED(hr) && pszPath) { - myDocumentsDirectory.translate(pszPath); + myDocumentsDirectory = widePathToFileApiPath(pszPath); CoTaskMemFree(pszPath); } } else { - char temp[_MAX_PATH + 1]; - if (SHGetSpecialFolderPath(nullptr, temp, CSIDL_PERSONAL, true)) { - myDocumentsDirectory = temp; + wchar_t temp[_MAX_PATH + 1]; + if (SHGetSpecialFolderPathW(nullptr, temp, CSIDL_PERSONAL, true)) { + myDocumentsDirectory = widePathToFileApiPath(temp); } } diff --git a/GeneralsMD/Code/GameEngine/Source/Common/System/DataChunk.cpp b/GeneralsMD/Code/GameEngine/Source/Common/System/DataChunk.cpp index 0d5d2ce7fe6..27e85a45bd2 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/System/DataChunk.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/System/DataChunk.cpp @@ -57,6 +57,11 @@ Bool CachedFileInputStream::open(AsciiString path) if (m_size) { m_buffer = file->readEntireAndClose(); file = nullptr; + if (m_buffer == nullptr) + { + m_size = 0; + return FALSE; + } } m_pos=0; } @@ -246,12 +251,22 @@ DataChunkOutput::~DataChunkOutput() m_contents.write(*m_pOut); // Rewind the temp m_tmp_file + if (m_tmp_file == nullptr) + { + DEBUG_CRASH(("DataChunkOutput - temp file was never opened")); + return; + } ::fclose(m_tmp_file); AsciiString tmpFileName = TheGlobalData->getPath_UserData(); tmpFileName.concat(TEMP_FILENAME); m_tmp_file = ::fopen( tmpFileName.str(), "rb" ); + if (m_tmp_file == nullptr) + { + DEBUG_CRASH(("DataChunkOutput - could not reopen temp file '%s'", tmpFileName.str())); + return; + } ::fseek(m_tmp_file, 0, SEEK_SET); // append the temp m_tmp_file m_contents diff --git a/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp b/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp index 368cd590513..6649a0c9f65 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp @@ -669,7 +669,23 @@ SaveCode GameState::loadGame( AvailableGameInfo gameInfo ) // open the save file XferLoad xferLoad; - xferLoad.open( filepath ); + try + { + xferLoad.open( filepath ); + } + catch( ... ) + { + // the file vanished or is locked, nothing has been reset yet so just report it + UnicodeString ufilepath; + ufilepath.translate(filepath); + + UnicodeString msg; + msg.format( TheGameText->fetch("GUI:ErrorLoadingGame"), ufilepath.str() ); + + MessageBoxOk(TheGameText->fetch("GUI:Error"), msg, nullptr); + + return SC_FILE_NOT_FOUND; + } // clear out the game engine TheGameEngine->reset(); @@ -960,7 +976,7 @@ Bool GameState::doesSaveGameExist( AsciiString filename ) // ------------------------------------------------------------------------------------------------ /** Get save game info from the filename specified */ // ------------------------------------------------------------------------------------------------ -void GameState::getSaveGameInfoFromFile( AsciiString filename, SaveGameInfo *saveGameInfo ) +Bool GameState::getSaveGameInfoFromFile( AsciiString filename, SaveGameInfo *saveGameInfo ) { AsciiString token; Int blockSize; @@ -972,13 +988,20 @@ void GameState::getSaveGameInfoFromFile( AsciiString filename, SaveGameInfo *sav { DEBUG_CRASH(( "GameState::getSaveGameInfoFromFile - Illegal parameters" )); - return; + return FALSE; } // open file for partial loading XferLoad xferLoad; - xferLoad.open( filename ); + try + { + xferLoad.open( filename ); + } + catch( ... ) + { + return FALSE; + } // // disable post processing cause we're not really doing a load of game data that @@ -986,7 +1009,11 @@ void GameState::getSaveGameInfoFromFile( AsciiString filename, SaveGameInfo *sav // xferLoad.setOptions( XO_NO_POST_PROCESSING ); + Bool found = FALSE; + // read all data blocks in the file + try + { while( done == FALSE ) { @@ -1037,6 +1064,7 @@ void GameState::getSaveGameInfoFromFile( AsciiString filename, SaveGameInfo *sav // data was found, copy game state info over *saveGameInfo = *tempGameState.getSaveGameInfo(); + found = TRUE; // we're all done with this file now done = TRUE; @@ -1056,10 +1084,19 @@ void GameState::getSaveGameInfoFromFile( AsciiString filename, SaveGameInfo *sav } } + } + catch( ... ) + { + // a corrupt or truncated file just isn't a usable save + xferLoad.close(); + return FALSE; + } // close the file xferLoad.close(); + return found; + } // ------------------------------------------------------------------------------------------------ @@ -1076,7 +1113,8 @@ static void addGameToAvailableList( AsciiString filename, void *userData ) try { // get header info from this listbox SaveGameInfo saveGameInfo; - TheGameState->getSaveGameInfoFromFile( filename, &saveGameInfo ); + if( TheGameState->getSaveGameInfoFromFile( filename, &saveGameInfo ) == FALSE ) + return; // allocate new info AvailableGameInfo *newInfo = new AvailableGameInfo; @@ -1266,7 +1304,10 @@ void GameState::iterateSaveFiles( IterateSaveFileCallback callback, void *userDa // start search hFile = FindFirstFile( "*", &item ); if( hFile == INVALID_HANDLE_VALUE ) + { + SetCurrentDirectory( currentDirectory ); return; + } // we are no longer on our first item first = FALSE; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameStateMap.cpp b/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameStateMap.cpp index e4057379f31..df0197f1654 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameStateMap.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameStateMap.cpp @@ -484,7 +484,10 @@ void GameStateMap::clearScratchPadMaps() // start search hFile = FindFirstFile( "*", &item ); if( hFile == INVALID_HANDLE_VALUE ) + { + SetCurrentDirectory( currentDirectory ); return; + } // we are no longer on our first item first = FALSE; diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/LanGameOptionsMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/LanGameOptionsMenu.cpp index 3866e6a1671..53cda5a2a2e 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/LanGameOptionsMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/LanGameOptionsMenu.cpp @@ -329,7 +329,7 @@ void StartPressed() } else { - mapDisplayName.format(L"%hs", myGame->getMap().str()); + mapDisplayName.translate(myGame->getMap().str()); willTransfer = WouldMapTransfer(myGame->getMap()); } for( i = 0; i < MAX_SLOTS; i++ ) @@ -955,7 +955,7 @@ void updateGameOptions() { s = s.reverseFind('\\') + 1; } - mapDisplayName.format(L"%hs", s.str()); + mapDisplayName.translate(s.str()); } UnicodeString old = GadgetStaticTextGetText(textEntryMapDisplay); if(old.compare(mapDisplayName) != 0) diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp index 0f4a9c7edce..dbcb3422e87 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp @@ -1033,7 +1033,7 @@ static void StartPressed() } else { - mapDisplayName.format(L"%hs", myGame->getMap().str()); + mapDisplayName.translate(myGame->getMap().str()); willTransfer = WouldMapTransfer(myGame->getMap()); } for( int i = 0; i < MAX_SLOTS; i++ ) @@ -1967,7 +1967,7 @@ void WOLGameSetupMenuInit( WindowLayout *layout, void *userData ) } else { - mapDisplayName.format(L"%hs", TheNGMPGame->getMap().str()); + mapDisplayName.translate(TheNGMPGame->getMap().str()); willTransfer = WouldMapTransfer(TheNGMPGame->getMap()); } @@ -3176,7 +3176,7 @@ void WOLGameSetupMenuUpdate( WindowLayout * layout, void *userData) } else { - mapDisplayName.format(L"%hs", TheGameState->getMapLeafName(game->getMap()).str()); + mapDisplayName.translate(TheGameState->getMapLeafName(game->getMap()).str()); willTransfer = WouldMapTransfer(game->getMap()); } if (willTransfer) @@ -3334,7 +3334,7 @@ void WOLGameSetupMenuUpdate( WindowLayout * layout, void *userData) } else { - mapDisplayName.format(L"%hs", game->getMap().str()); + mapDisplayName.translate(game->getMap().str()); willTransfer = WouldMapTransfer(game->getMap()); } UnicodeString text; diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLMapSelectMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLMapSelectMenu.cpp index 20e2490f850..d898c52b9ff 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLMapSelectMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLMapSelectMenu.cpp @@ -457,7 +457,7 @@ WindowMsgHandledType WOLMapSelectMenuSystem( GameWindow *window, UnsignedInt msg asciiMap.translate( map ); int newMaxPlayers = -1; - AsciiString strMapName; + UnicodeString strMapName; bool bOfficialMap = false; TheNGMPGame->setMap(asciiMap); @@ -470,7 +470,7 @@ WindowMsgHandledType WOLMapSelectMenuSystem( GameWindow *window, UnsignedInt msg TheNGMPGame->setMapSize( it->second.m_filesize ); newMaxPlayers = it->second.m_numPlayers; - strMapName.translate(it->second.m_displayName); + strMapName = it->second.m_displayName; bOfficialMap = it->second.m_isOfficial; } diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLQuickMatchMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLQuickMatchMenu.cpp index d5ec34b9fa3..0cdabf7d55e 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLQuickMatchMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLQuickMatchMenu.cpp @@ -1225,7 +1225,7 @@ void WOLQuickMatchMenuInit( WindowLayout *layout, void *userData ) for (PlaylistEntry& playlist : vecPlaylists) { - s.format(L"%hs", playlist.Name.c_str()); + s = UnicodeString(from_utf8(playlist.Name).c_str()); GadgetComboBoxAddEntry(comboBoxNumPlayers, s, GameSpyColor[GSCOLOR_DEFAULT]); } @@ -1241,7 +1241,7 @@ void WOLQuickMatchMenuInit( WindowLayout *layout, void *userData ) pLobbyInterface->RegisterForMatchmakingMessageCallback([](std::string strMsg) { UnicodeString uMsg; - uMsg.format(L"%hs", strMsg.c_str()); + uMsg = UnicodeString(from_utf8(strMsg).c_str()); Int index = GadgetListBoxAddEntryText(quickmatchTextWindow, uMsg, GameSpyColor[GSCOLOR_DEFAULT], -1, -1); GadgetListBoxSetItemData(quickmatchTextWindow, (void*)-1, index); diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/GeneralsOnline_Settings.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/GeneralsOnline_Settings.cpp index d8c249e5c82..fb8e72f33e0 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/GeneralsOnline_Settings.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/GeneralsOnline_Settings.cpp @@ -72,21 +72,31 @@ void GenOnlineSettings::Load(void) std::string strSettingsFilePath = std::format("{}/{}", strSettingsFileDir, SETTINGS_FILENAME); std::string strSettingsFilePathLegacy = std::format("{}/{}", GameDir, SETTINGS_FILENAME_LEGACY); + std::error_code ec; + // create directories we need - if (!std::filesystem::exists(strSettingsFileDir)) + if (!std::filesystem::exists(strSettingsFileDir, ec)) { - std::filesystem::create_directory(strSettingsFileDir); + std::filesystem::create_directory(strSettingsFileDir, ec); } // NGMP_NOTE: Prior to 6/23, we used the game dir for settings, this code migrates any legacy settings file to the new location (game user data dir) - if (std::filesystem::exists(strSettingsFilePathLegacy)) + if (std::filesystem::exists(strSettingsFilePathLegacy, ec)) { - std::filesystem::copy(strSettingsFilePathLegacy, strSettingsFilePath, std::filesystem::copy_options::overwrite_existing); - std::filesystem::remove(strSettingsFilePathLegacy); + // only drop the legacy file once its copy is in place + std::filesystem::copy(strSettingsFilePathLegacy, strSettingsFilePath, std::filesystem::copy_options::overwrite_existing, ec); + if (!ec) + { + std::filesystem::remove(strSettingsFilePathLegacy, ec); + } } bool bApplyDefaults = false; + // a settings file we couldn't read or parse must not be silently replaced by defaults + bool bReadFailed = false; + bool bParseFailed = false; + std::vector vecBytes; FILE* file = fopen(strSettingsFilePath.c_str(), "rb"); if (file) @@ -97,10 +107,22 @@ void GenOnlineSettings::Load(void) if (fileSize > 0) { vecBytes.resize(fileSize); - fread(vecBytes.data(), 1, fileSize, file); + if (fread(vecBytes.data(), 1, fileSize, file) != static_cast(fileSize)) + { + vecBytes.clear(); + bReadFailed = true; + } + } + else if (fileSize < 0) + { + bReadFailed = true; } fclose(file); } + else if (errno != ENOENT) + { + bReadFailed = true; + } if (!vecBytes.empty()) @@ -117,9 +139,11 @@ void GenOnlineSettings::Load(void) { jsonSettings = nullptr; bApplyDefaults = true; + bParseFailed = true; } if (!bApplyDefaults && jsonSettings != nullptr) + try { if (jsonSettings.contains(SETTINGS_KEY_CAMERA)) { @@ -271,6 +295,12 @@ void GenOnlineSettings::Load(void) } } } + catch (...) + { + // a value of the wrong type + bApplyDefaults = true; + bParseFailed = true; + } } else // setup defaults @@ -281,7 +311,7 @@ void GenOnlineSettings::Load(void) if (bApplyDefaults) { m_Camera_MinHeight = m_Camera_MinHeight_default; - m_Camera_MaxHeight_LobbyHost = m_Camera_MaxHeight_LobbyHost; + m_Camera_MaxHeight_LobbyHost = GENERALS_ONLINE_DEFAULT_LOBBY_CAMERA_ZOOM; m_bVerbose = false; m_Render_LimitFramerate = true; m_Render_FramerateLimit_FPSVal = 60; @@ -294,6 +324,19 @@ void GenOnlineSettings::Load(void) m_Social_Notification_FriendGoesOffline_Gameplay = true; } + if (bReadFailed) + { + // keep the file on disk, the defaults above only apply to this session + NetworkLog(ELogVerbosity::LOG_RELEASE, "[NGMP] Could not read settings file, keeping it untouched"); + return; + } + + if (bParseFailed) + { + // keep the unparseable file for recovery before the defaults replace it + std::filesystem::rename(strSettingsFilePath, strSettingsFilePath + ".bad", ec); + } + // Always save so we re-serialize anything new or missing Save(); } @@ -378,10 +421,25 @@ void GenOnlineSettings::Save() std::string strData = root.dump(1); std::string strSettingsFilePath = std::format("{}/GeneralsOnlineData/{}", TheGlobalData->getPath_UserData().str(), SETTINGS_FILENAME); - FILE* file = fopen(strSettingsFilePath.c_str(), "wb"); + + // write to a temp file and swap it in so a failed write never leaves a truncated settings file + std::string strTempPath = strSettingsFilePath + ".tmp"; + bool bSaved = false; + FILE* file = fopen(strTempPath.c_str(), "wb"); if (file) { - fwrite(strData.data(), 1, strData.size(), file); - fclose(file); + bSaved = fwrite(strData.data(), 1, strData.size(), file) == strData.size(); + if (fclose(file) != 0) + bSaved = false; + } + + if (bSaved) + bSaved = MoveFileExA(strTempPath.c_str(), strSettingsFilePath.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) != 0; + + if (!bSaved) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[NGMP] Failed to save settings file"); + std::error_code ec; + std::filesystem::remove(strTempPath, ec); } } diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMPGame.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMPGame.cpp index 834bd86851a..62f7b703cba 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMPGame.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMPGame.cpp @@ -570,7 +570,7 @@ void NGMPGame::launchGame(void) if (pos != std::string::npos) { strMapName.erase(pos, strExt.size()); } UnicodeString msg; - msg.format(L"Map: %hs\nPress F5 or INSERT to open the communicator.", strMapName.c_str()); + msg.format(L"Map: %s\nPress F5 or INSERT to open the communicator.", from_utf8(strMapName).c_str()); showNotificationBox(AsciiString::TheEmptyString, msg, false); } diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp index 3a943805065..77223c06347 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp @@ -36,6 +36,44 @@ std::wstring from_utf8(const std::string& utf8_str) return result; } +#ifndef WC_NO_BEST_FIT_CHARS +#define WC_NO_BEST_FIT_CHARS 0x00000400 +#endif + +// Process code page text (paths from local file APIs) to UTF-8 for the wire +std::string local_to_utf8(const std::string& local_str) +{ + if (local_str.empty() || GetACP() == CP_UTF8) + return local_str; + + int wlen = MultiByteToWideChar(CP_ACP, 0, local_str.data(), (int)local_str.size(), nullptr, 0); + if (wlen <= 0) + return local_str; + + std::wstring wide(wlen, L'\0'); + MultiByteToWideChar(CP_ACP, 0, local_str.data(), (int)local_str.size(), wide.data(), wlen); + return to_utf8(wide); +} + +// UTF-8 from the wire to the process code page that local file APIs expect; unrepresentable characters become '_' +std::string utf8_to_local(const std::string& utf8_str) +{ + if (utf8_str.empty() || GetACP() == CP_UTF8) + return utf8_str; + + std::wstring wide = from_utf8(utf8_str); + if (wide.empty()) + return std::string(); + + int len = WideCharToMultiByte(CP_ACP, WC_NO_BEST_FIT_CHARS, wide.data(), (int)wide.size(), nullptr, 0, "_", nullptr); + if (len <= 0) + return utf8_str; + + std::string result(len, '\0'); + WideCharToMultiByte(CP_ACP, WC_NO_BEST_FIT_CHARS, wide.data(), (int)wide.size(), result.data(), len, "_", nullptr); + return result; +} + // Legacy format strings take player names as narrow %hs arguments, which get widened byte by byte and mangle UTF-8. // Rewriting %hs to %s lets callers pass a from_utf8() decoded name instead. std::wstring WidenFormatSpecifiers(const std::wstring& format) diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp index 810c2438af5..e72240f63f7 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp @@ -611,9 +611,10 @@ void NGMP_OnlineServices_AuthInterface::LogoutOfMyAccount() // delete local credentials cache std::string strCredentialsCachePath = GetCredentialsFilePath(); - if (std::filesystem::exists(strCredentialsCachePath)) + std::error_code ec; + if (std::filesystem::exists(strCredentialsCachePath, ec)) { - std::filesystem::remove(strCredentialsCachePath); + std::filesystem::remove(strCredentialsCachePath, ec); } } @@ -632,30 +633,49 @@ void NGMP_OnlineServices_AuthInterface::SaveCredentials(const char* szRefreshTok std::string strData = root.dump(1); - FILE* file = fopen(GetCredentialsFilePath().c_str(), "wb"); - if (file) - { + // encrypt before touching the file so a failure doesn't truncate the credentials already stored + std::string strFileData; #if defined(GENERALS_ONLINE_ENCRYPT_CREDENTIALS) - DATA_BLOB inputBlob; - DATA_BLOB outputBlob; + DATA_BLOB inputBlob; + DATA_BLOB outputBlob; - inputBlob.pbData = (BYTE*)strData.c_str(); - inputBlob.cbData = static_cast(strData.size()); + inputBlob.pbData = (BYTE*)strData.c_str(); + inputBlob.cbData = static_cast(strData.size()); - if (CryptProtectData(&inputBlob, L"GO Credentials", nullptr, nullptr, nullptr, 0, &outputBlob)) - { - fwrite(outputBlob.pbData, 1, outputBlob.cbData, file); - LocalFree(outputBlob.pbData); - } - else - { - // TODO_JWT: Handle failure case - } + if (CryptProtectData(&inputBlob, L"GO Credentials", nullptr, nullptr, nullptr, 0, &outputBlob)) + { + strFileData.assign((const char*)outputBlob.pbData, outputBlob.cbData); + LocalFree(outputBlob.pbData); + } + else + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[NGMP] Failed to encrypt credentials, keeping the stored ones"); + return; + } #else - fwrite(strData.data(), 1, strData.size(), file); + strFileData = strData; #endif - fclose(file); + // write to a temp file and swap it in so a failed write never leaves truncated credentials + std::string strCredentialsPath = GetCredentialsFilePath(); + std::string strTempPath = strCredentialsPath + ".tmp"; + bool bSaved = false; + FILE* file = fopen(strTempPath.c_str(), "wb"); + if (file) + { + bSaved = fwrite(strFileData.data(), 1, strFileData.size(), file) == strFileData.size(); + if (fclose(file) != 0) + bSaved = false; + } + + if (bSaved) + bSaved = MoveFileExA(strTempPath.c_str(), strCredentialsPath.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) != 0; + + if (!bSaved) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[NGMP] Failed to save credentials"); + std::error_code ec; + std::filesystem::remove(strTempPath, ec); } } diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp index b23632f803e..741f156d0c4 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp @@ -471,15 +471,20 @@ void NGMP_OnlineServicesManager::ContinueUpdate() std::map mapHeaders; m_pHTTPManager->SendGETRequest(strDownloadPath.c_str(), EIPProtocolVersion::DONT_CARE, mapHeaders, [=](bool bSuccess, int statusCode, std::string strBody, HTTPRequest* pReq) { + auto fnShowUpdateFailed = []() + { + // show msg + ClearGSMessageBoxes(); + MessageBoxOk(UnicodeString(L"Update Failed"), UnicodeString(L"Could not download the updater. Press below to exit."), []() + { + TheGameEngine->setQuitting(TRUE); + }); + ShellExecuteA(NULL, "open", "https://www.playgenerals.online/updatefailed", NULL, NULL, SW_SHOWNORMAL); + }; + if (statusCode != 200) { - // show msg - ClearGSMessageBoxes(); - MessageBoxOk(UnicodeString(L"Update Failed"), UnicodeString(L"Could not download the updater. Press below to exit."), []() - { - TheGameEngine->setQuitting(TRUE); - }); - ShellExecuteA(NULL, "open", "https://www.playgenerals.online/updatefailed", NULL, NULL, SW_SHOWNORMAL); + fnShowUpdateFailed(); } else { @@ -502,15 +507,26 @@ void NGMP_OnlineServicesManager::ContinueUpdate() std::vector vecBuffer = pReq->GetBuffer(); size_t bufSize = pReq->GetBufferSize(); - if (!std::filesystem::exists(strPatchDir)) + std::error_code ec; + if (!std::filesystem::exists(strPatchDir, ec)) { - std::filesystem::create_directory(strPatchDir); + std::filesystem::create_directory(strPatchDir, ec); } + // a partially written updater must not be treated as downloaded + bool bSaved = false; FILE* pFile = fopen(strOutPath.c_str(), "wb"); if (pFile != nullptr) { - fwrite(vecBuffer.data(), sizeof(uint8_t), bufSize, pFile); - fclose(pFile); + bSaved = fwrite(vecBuffer.data(), sizeof(uint8_t), bufSize, pFile) == bufSize; + if (fclose(pFile) != 0) + bSaved = false; + } + + if (!bSaved) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "Failed to save downloaded file: %s", strOutPath.c_str()); + fnShowUpdateFailed(); + return; } // call continue update again, thisll check if we're done or have more work to do @@ -1059,16 +1075,27 @@ void NGMP_OnlineServicesManager::InitSentry() curl_global_init(CURL_GLOBAL_DEFAULT); #if !_DEBUG - std::string strDumpPath = std::format("{}/GeneralsOnlineCrashData/", TheGlobalData->getPath_UserData().str()); - if (!std::filesystem::exists(strDumpPath)) + // game paths are in the process code page, sentry's char* API wants UTF-8, so hand it the wide path + std::wstring strDumpPath; + const AsciiString& strUserDataPath = TheGlobalData->getPath_UserData(); + const int wideLen = MultiByteToWideChar(CP_ACP, 0, strUserDataPath.str(), -1, nullptr, 0); + if (!strUserDataPath.isEmpty() && wideLen > 1) { - std::filesystem::create_directory(strDumpPath); + std::wstring strUserDataPathW(wideLen - 1, L'\0'); + MultiByteToWideChar(CP_ACP, 0, strUserDataPath.str(), -1, strUserDataPathW.data(), wideLen); + strDumpPath = strUserDataPathW + L"GeneralsOnlineCrashData\\"; + + std::error_code ec; + std::filesystem::create_directories(strDumpPath, ec); } sentry_options_t* options = sentry_options_new(); sentry_options_set_dsn(options, "https://61750bebd112d279bcc286d617819269@o4509316925554688.ingest.us.sentry.io/4509316927586304"); - sentry_options_set_database_path(options, strDumpPath.c_str()); + if (!strDumpPath.empty()) + { + sentry_options_set_database_pathw(options, strDumpPath.c_str()); + } std::string strVersionStr = std::format("generalsonline-client@{}", GENERALS_ONLINE_VERSION_STRING); sentry_options_set_release(options, strVersionStr.c_str()); diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp index 664f69f1a86..01dfdfdcc59 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp @@ -36,7 +36,7 @@ UnicodeString NGMP_OnlineServices_LobbyInterface::GetCurrentLobbyMapDisplayName( if (IsInLobby()) { - strDisplayName.format(L"%hs", m_CurrentLobby.map_name.c_str()); + strDisplayName = UnicodeString(from_utf8(m_CurrentLobby.map_name).c_str()); } return strDisplayName; @@ -77,7 +77,7 @@ enum class ELobbyUpdateField JOINABILITY = 18 }; -void NGMP_OnlineServices_LobbyInterface::UpdateCurrentLobby_Map(AsciiString strMap, AsciiString strMapPath, bool bIsOfficial, int newMaxPlayers) +void NGMP_OnlineServices_LobbyInterface::UpdateCurrentLobby_Map(UnicodeString strMap, AsciiString strMapPath, bool bIsOfficial, int newMaxPlayers) { // reset autostart if host changes anything (because ready flag will reset too) #if !defined(GENERALS_ONLINE_DISABLE_AUTO_ACCEPT) @@ -101,8 +101,8 @@ void NGMP_OnlineServices_LobbyInterface::UpdateCurrentLobby_Map(AsciiString strM nlohmann::json j; j["field"] = ELobbyUpdateField::LOBBY_MAP; - j["map"] = strMap.str(); - j["map_path"] = sanitizedMapPath.str(); + j["map"] = to_utf8(strMap.str()); + j["map_path"] = local_to_utf8(sanitizedMapPath.str()); j["map_official"] = bIsOfficial; j["max_players"] = newMaxPlayers; std::string strPostData = j.dump(); @@ -578,6 +578,7 @@ void NGMP_OnlineServices_LobbyInterface::SearchForLobbies(std::function lobbyEntryIter["Name"].get_to(lobbyEntry.name); lobbyEntryIter["MapName"].get_to(lobbyEntry.map_name); lobbyEntryIter["MapPath"].get_to(lobbyEntry.map_path); + lobbyEntry.map_path = utf8_to_local(lobbyEntry.map_path); // local file path from here on lobbyEntryIter["IsMapOfficial"].get_to(lobbyEntry.map_official); lobbyEntryIter["NumCurrentPlayers"].get_to(lobbyEntry.current_players); lobbyEntryIter["MaxPlayers"].get_to(lobbyEntry.max_players); @@ -907,6 +908,7 @@ void NGMP_OnlineServices_LobbyInterface::UpdateRoomDataCache(std::functionm_exeCRC; j["ini_crc"] = TheGlobalData->m_iniCRC; diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_MatchmakingInterface.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_MatchmakingInterface.cpp index bc485a6e1e1..374120d9a46 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_MatchmakingInterface.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_MatchmakingInterface.cpp @@ -41,6 +41,7 @@ void NGMP_OnlineServices_MatchmakingInterface::RetrievePlaylists(std::functiongetPath_UserData().str()); + snprintf(m_filePath, ARRAY_SIZE(m_filePath), USER_W3D_DIR_PATH, TheGlobalData->getPath_UserData().str()); strlcat(m_filePath, filename, ARRAY_SIZE(m_filePath)); } else if( isImageFileType(fileType) ) { - sprintf(m_filePath,USER_TGA_DIR_PATH, TheGlobalData->getPath_UserData().str()); + snprintf(m_filePath, ARRAY_SIZE(m_filePath), USER_TGA_DIR_PATH, TheGlobalData->getPath_UserData().str()); strlcat(m_filePath, filename, ARRAY_SIZE(m_filePath)); } @@ -304,7 +304,7 @@ char const * GameFileClass::Set_Name( char const *filename ) { if( fileType == FILE_TYPE_TGA ) // just TGA, since we don't do dds previews { - sprintf(m_filePath,MAP_PREVIEW_DIR_PATH, TheGlobalData->getPath_UserData().str()); + snprintf(m_filePath, ARRAY_SIZE(m_filePath), MAP_PREVIEW_DIR_PATH, TheGlobalData->getPath_UserData().str()); strlcat(m_filePath, filename, ARRAY_SIZE(m_filePath)); } diff --git a/GeneralsMD/Code/GameEngineDevice/Source/Win32Device/Common/Win32GameEngine.cpp b/GeneralsMD/Code/GameEngineDevice/Source/Win32Device/Common/Win32GameEngine.cpp index 9c7ac114db8..a9535aa2d5a 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/Win32Device/Common/Win32GameEngine.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/Win32Device/Common/Win32GameEngine.cpp @@ -148,11 +148,12 @@ void Win32GameEngine::serviceWindowsOS() // see if we have any messages to process, a nullptr window handle tells the // OS to look at the main window associated with the calling thread, us! // - while( PeekMessage( &msg, nullptr, 0, 0, PM_NOREMOVE ) ) + // W variants: an A pump would convert the Unicode window's WM_CHAR back to the code page + while( PeekMessageW( &msg, nullptr, 0, 0, PM_NOREMOVE ) ) { // get the message - returnValue = GetMessage( &msg, nullptr, 0, 0 ); + returnValue = GetMessageW( &msg, nullptr, 0, 0 ); // this is one possible way to check for quitting conditions as a message // of WM_QUIT will cause GetMessage() to return 0 @@ -169,7 +170,7 @@ void Win32GameEngine::serviceWindowsOS() TheMessageTime = msg.time; // translate and dispatch the message TranslateMessage( &msg ); - DispatchMessage( &msg ); + DispatchMessageW( &msg ); TheMessageTime = 0; } diff --git a/GeneralsMD/Code/Main/WinMain.cpp b/GeneralsMD/Code/Main/WinMain.cpp index 90e852b502a..76552f857b6 100644 --- a/GeneralsMD/Code/Main/WinMain.cpp +++ b/GeneralsMD/Code/Main/WinMain.cpp @@ -687,7 +687,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, } return 0;*/ - return DefWindowProc(hWnd, message, wParam, lParam); + return DefWindowProcW(hWnd, message, wParam, lParam); } @@ -700,14 +700,14 @@ static Bool initializeAppWindows(HINSTANCE hInstance, Int nCmdShow, Bool runWind Int startWidth = DEFAULT_DISPLAY_WIDTH, startHeight = DEFAULT_DISPLAY_HEIGHT; - // register the window class + // register the window class, Unicode so WM_CHAR and WM_IME_CHAR carry UTF-16 regardless of the code page - WNDCLASS wndClass = { CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS, WndProc, 0, 0, hInstance, - LoadIcon(hInstance, MAKEINTRESOURCE(IDI_ApplicationIcon)), + WNDCLASSW wndClass = { CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS, WndProc, 0, 0, hInstance, + LoadIconW(hInstance, MAKEINTRESOURCEW(IDI_ApplicationIcon)), nullptr/*LoadCursor(nullptr, IDC_ARROW)*/, (HBRUSH)GetStockObject(BLACK_BRUSH), nullptr, - TEXT("Game Window") }; - RegisterClass(&wndClass); + L"Game Window" }; + RegisterClassW(&wndClass); // Create our main window windowStyle = WS_POPUP | WS_VISIBLE; @@ -730,8 +730,8 @@ static Bool initializeAppWindows(HINSTANCE hInstance, Int nCmdShow, Bool runWind gInitializing = true; - HWND hWnd = CreateWindow(TEXT("Game Window"), - TEXT("Command and Conquer Generals"), + HWND hWnd = CreateWindowW(L"Game Window", + L"Command and Conquer Generals", windowStyle, (GetSystemMetrics(SM_CXSCREEN) / 2) - (startWidth / 2), // original position X (GetSystemMetrics(SM_CYSCREEN) / 2) - (startHeight / 2),// original position Y diff --git a/GeneralsMD/Code/Main/app.manifest b/GeneralsMD/Code/Main/app.manifest index 207a0886b37..77f28a3c213 100644 --- a/GeneralsMD/Code/Main/app.manifest +++ b/GeneralsMD/Code/Main/app.manifest @@ -23,6 +23,8 @@ true/pm PerMonitorV2 + + UTF-8