diff --git a/Core/GameEngine/Include/Common/FileSystem.h b/Core/GameEngine/Include/Common/FileSystem.h index 2aaa30a61f1..6cf770cabbc 100644 --- a/Core/GameEngine/Include/Common/FileSystem.h +++ b/Core/GameEngine/Include/Common/FileSystem.h @@ -163,6 +163,12 @@ class FileSystem : public SubsystemInterface static bool removeExtension(AsciiString& path); static bool removeExtension(UnicodeString& path); + static AsciiString normalizePathSeparators(const AsciiString& path); ///< converts game-data path separators to the native separator. + + /// Appends the native separator to nonempty paths unless either separator is already at the end. + /// Existing separators are not converted. + static void appendPathSeparator(AsciiString& path); + protected: #if ENABLE_FILESYSTEM_EXISTENCE_CACHE struct FileExistData diff --git a/Core/GameEngine/Source/Common/INI/INIMapCache.cpp b/Core/GameEngine/Source/Common/INI/INIMapCache.cpp index 5be4414bb70..8d73dce48d6 100644 --- a/Core/GameEngine/Source/Common/INI/INIMapCache.cpp +++ b/Core/GameEngine/Source/Common/INI/INIMapCache.cpp @@ -32,6 +32,7 @@ #include "Lib/BaseType.h" #include "Common/INI.h" +#include "Common/FileSystem.h" #include "GameClient/MapUtil.h" #include "GameClient/GameText.h" #include "GameNetwork/NetworkDefs.h" @@ -194,7 +195,7 @@ void INI::parseMapCacheDefinition( INI* ini ) if(TheMapCache && !md.m_displayName.isEmpty()) { - AsciiString lowerName = name; + AsciiString lowerName = FileSystem::normalizePathSeparators(name); lowerName.toLower(); md.m_fileName = lowerName; // DEBUG_LOG(("INI::parseMapCacheDefinition - adding %s to map cache", lowerName.str())); diff --git a/Core/GameEngine/Source/Common/System/FileSystem.cpp b/Core/GameEngine/Source/Common/System/FileSystem.cpp index b8e4c4695b6..0b83b5efec4 100644 --- a/Core/GameEngine/Source/Common/System/FileSystem.cpp +++ b/Core/GameEngine/Source/Common/System/FileSystem.cpp @@ -358,13 +358,9 @@ Bool FileSystem::isPathInDirectory(const AsciiString& testPath, const AsciiStrin return false; } -#ifdef _WIN32 - const char* pathSep = "\\"; -#else - const char* pathSep = "/"; -#endif + const char pathSep = getNativePathSeparator(); - if (!basePathNormalized.endsWith(pathSep)) + if (basePathNormalized.getCharAt(basePathNormalized.getLength() - 1) != pathSep) { basePathNormalized.concat(pathSep); } @@ -408,3 +404,30 @@ bool FileSystem::removeExtension(UnicodeString& path) return false; } + +//============================================================================ +// FileSystem::normalizePathSeparators +//============================================================================ +AsciiString FileSystem::normalizePathSeparators(const AsciiString& path) +{ + const char otherSeparator = getNativePathSeparator() == '/' ? '\\' : '/'; + if (path.find(otherSeparator) == nullptr) + { + return path; + } + + AsciiString normalized; + ::normalizePathSeparators(normalized.getBufferForRead(path.getLength()), path.str()); + return normalized; +} + +//============================================================================ +// FileSystem::appendPathSeparator +//============================================================================ +void FileSystem::appendPathSeparator(AsciiString& path) +{ + if (path.isNotEmpty() && !isPathSeparator(path.getCharAt(path.getLength() - 1))) + { + path.concat(getNativePathSeparator()); + } +} diff --git a/Core/GameEngine/Source/Common/UserPreferences.cpp b/Core/GameEngine/Source/Common/UserPreferences.cpp index afc017cd9c0..fa039b953a5 100644 --- a/Core/GameEngine/Source/Common/UserPreferences.cpp +++ b/Core/GameEngine/Source/Common/UserPreferences.cpp @@ -38,6 +38,7 @@ //----------------------------------------------------------------------------- #include "Common/GameSpyMiscPreferences.h" #include "Common/UserPreferences.h" +#include "Common/FileSystem.h" #include "Common/LadderPreferences.h" #include "Common/Player.h" #include "Common/PlayerTemplate.h" @@ -668,7 +669,7 @@ AsciiString CustomMatchPreferences::getPreferredMap() return ret; } - ret = QuotedPrintableToAsciiString(it->second); + ret = FileSystem::normalizePathSeparators(QuotedPrintableToAsciiString(it->second)); ret.trim(); if (ret.isEmpty() || !isValidMap(ret, TRUE)) { //map is invalid, use default instead diff --git a/Core/GameEngine/Source/GameClient/MapUtil.cpp b/Core/GameEngine/Source/GameClient/MapUtil.cpp index 1806c50741a..5a628af0bb2 100644 --- a/Core/GameEngine/Source/GameClient/MapUtil.cpp +++ b/Core/GameEngine/Source/GameClient/MapUtil.cpp @@ -561,7 +561,7 @@ Bool MapCache::loadMapsFromDisk( const AsciiString &mapDir, Bool isOfficial, Boo continue; } - mapListChanged |= addMap(mapDir, *filepathIt, filepathLower, fileInfo, isOfficial); + mapListChanged |= addMap(mapDir, *filepathIt, FileSystem::normalizePathSeparators(filepathLower), fileInfo, isOfficial); } if (clearUnseenMaps(mapDir)) @@ -1012,17 +1012,8 @@ Bool isValidMap( AsciiString mapName, Bool isMultiplayer ) return FALSE; TheMapCache->updateCache(); - mapName.toLower(); - MapCache::iterator it = TheMapCache->find(mapName); - if (it != TheMapCache->end()) - { - if (isMultiplayer == it->second.m_isMultiplayer) - { - return TRUE; - } - } - - return FALSE; + const MapMetaData *mapData = TheMapCache->findMap(mapName); + return mapData != nullptr && isMultiplayer == mapData->m_isMultiplayer; } //------------------------------------------------------------------------------------------------- @@ -1070,17 +1061,18 @@ Bool isOfficialMap( AsciiString mapName ) if(!TheMapCache || mapName.isEmpty()) return FALSE; TheMapCache->updateCache(); - mapName.toLower(); - MapCache::iterator it = TheMapCache->find(mapName); - if (it != TheMapCache->end()) - return it->second.m_isOfficial; - return FALSE; + const MapMetaData *mapData = TheMapCache->findMap(mapName); + return mapData != nullptr && mapData->m_isOfficial; } const MapMetaData *MapCache::findMap(AsciiString mapName) { mapName.toLower(); + + // TheSuperHackers @bugfix bobtista 14/09/2026 Use the same separators for cache keys and lookups. + mapName = FileSystem::normalizePathSeparators(mapName); + MapCache::iterator it = find(mapName); if (it == end()) return nullptr; diff --git a/Core/Libraries/Include/Lib/PathUtil.h b/Core/Libraries/Include/Lib/PathUtil.h index 683f6fee130..83a7d62fc85 100644 --- a/Core/Libraries/Include/Lib/PathUtil.h +++ b/Core/Libraries/Include/Lib/PathUtil.h @@ -23,7 +23,8 @@ #include "BaseType.h" #include -inline bool isPathSeparator(char ch) +// Returns true for a separator the host platform uses to open files. +inline bool isNativePathSeparator(char ch) { #ifdef _WIN32 return ch == '\\' || ch == '/'; @@ -32,6 +33,12 @@ inline bool isPathSeparator(char ch) #endif } +// Returns true for either separator. Game data paths carry '\\' on every platform. +inline bool isPathSeparator(char ch) +{ + return ch == '/' || ch == '\\'; +} + inline bool isAbsolutePath(const char* path) { if (path == nullptr) @@ -39,14 +46,14 @@ inline bool isAbsolutePath(const char* path) return false; } - if (isPathSeparator(path[0])) + if (isNativePathSeparator(path[0])) { return true; } #ifdef _WIN32 const bool hasDriveLetter = (path[0] >= 'A' && path[0] <= 'Z') || (path[0] >= 'a' && path[0] <= 'z'); - if (hasDriveLetter && path[1] == ':' && isPathSeparator(path[2])) + if (hasDriveLetter && path[1] == ':' && isNativePathSeparator(path[2])) { return true; } @@ -55,6 +62,44 @@ inline bool isAbsolutePath(const char* path) return false; } +inline char getNativePathSeparator() +{ +#ifdef _WIN32 + return '\\'; +#else + return '/'; +#endif +} + +// Copies a game-data path using native separators. Output needs strlen(path) + 1 bytes. +inline void normalizePathSeparators(char* output, const char* path) +{ + const char separator = getNativePathSeparator(); + while (*path) + { + *output++ = isPathSeparator(*path) ? separator : *path; + ++path; + } + *output = 0; +} + +inline const char* getLastPathSeparator(const char* path) +{ + return path ? maxPtr(strrchr(path, '/'), strrchr(path, '\\')) : nullptr; +} + +inline const wchar_t* getLastPathSeparator(const wchar_t* path) +{ + return path ? maxPtr(wcsrchr(path, L'/'), wcsrchr(path, L'\\')) : nullptr; +} + +// Returns the whole path when it contains no separator +inline const char* getFileName(const char* path) +{ + const char* lastSeparator = getLastPathSeparator(path); + return lastSeparator ? lastSeparator + 1 : path; +} + inline const char* getExtension(const char* path) { const char* lastDot = strrchr(path, '.'); @@ -64,7 +109,7 @@ inline const char* getExtension(const char* path) return nullptr; } - const char* lastSeparator = maxPtr(strrchr(path, '/'), strrchr(path, '\\')); + const char* lastSeparator = getLastPathSeparator(path); // Check if the dot is contained in the filename if (lastSeparator && lastDot < lastSeparator) @@ -84,7 +129,7 @@ inline const wchar_t* getExtension(const wchar_t* path) return nullptr; } - const wchar_t* lastSeparator = maxPtr(wcsrchr(path, L'/'), wcsrchr(path, L'\\')); + const wchar_t* lastSeparator = getLastPathSeparator(path); // Check if the dot is contained in the filename if (lastSeparator && lastDot < lastSeparator) diff --git a/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/LanGameOptionsMenu.cpp b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/LanGameOptionsMenu.cpp index 0520332611d..232c71d563f 100644 --- a/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/LanGameOptionsMenu.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/LanGameOptionsMenu.cpp @@ -780,15 +780,18 @@ void LanGameOptionsMenuInit( WindowLayout *layout, void *userData ) slot->setColor( pref.getPreferredColor() ); slot->setPlayerTemplate( pref.getPreferredFaction() ); slot->setNATBehavior(FirewallHelperClass::FIREWALL_TYPE_SIMPLE); - game->setMap( pref.getPreferredMap() ); - AsciiString lowerMap = pref.getPreferredMap(); - lowerMap.toLower(); - std::map::iterator it = TheMapCache->find(lowerMap); - if (it != TheMapCache->end()) + AsciiString mapName = pref.getPreferredMap(); + const MapMetaData *mapData = TheMapCache->findMap(mapName); + if (mapData != nullptr) + { + mapName = mapData->m_fileName; + } + game->setMap(mapName); + if (mapData != nullptr) { TheLAN->GetMyGame()->getSlot(0)->setMapAvailability(true); - TheLAN->GetMyGame()->setMapCRC( it->second.m_CRC ); - TheLAN->GetMyGame()->setMapSize( it->second.m_filesize ); + TheLAN->GetMyGame()->setMapCRC( mapData->m_CRC ); + TheLAN->GetMyGame()->setMapSize( mapData->m_filesize ); TheLAN->GetMyGame()->adjustSlotsForMap(); // BGC- adjust the slots for the selected map. } 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 609a0f772cd..9ed2aae7e9c 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/LanGameOptionsMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/LanGameOptionsMenu.cpp @@ -856,17 +856,20 @@ void LanGameOptionsMenuInit( WindowLayout *layout, void *userData ) slot->setColor( pref.getPreferredColor() ); slot->setPlayerTemplate( pref.getPreferredFaction() ); slot->setNATBehavior(FirewallHelperClass::FIREWALL_TYPE_SIMPLE); - game->setMap( pref.getPreferredMap() ); + AsciiString mapName = pref.getPreferredMap(); + const MapMetaData *mapData = TheMapCache->findMap(mapName); + if (mapData != nullptr) + { + mapName = mapData->m_fileName; + } + game->setMap(mapName); game->setStartingCash( pref.getStartingCash() ); game->setSuperweaponRestriction( pref.getSuperweaponRestricted() ? 1 : 0 ); - AsciiString lowerMap = pref.getPreferredMap(); - lowerMap.toLower(); - std::map::iterator it = TheMapCache->find(lowerMap); - if (it != TheMapCache->end()) + if (mapData != nullptr) { TheLAN->GetMyGame()->getSlot(0)->setMapAvailability(true); - TheLAN->GetMyGame()->setMapCRC( it->second.m_CRC ); - TheLAN->GetMyGame()->setMapSize( it->second.m_filesize ); + TheLAN->GetMyGame()->setMapCRC( mapData->m_CRC ); + TheLAN->GetMyGame()->setMapSize( mapData->m_filesize ); TheLAN->GetMyGame()->adjustSlotsForMap(); // BGC- adjust the slots for the selected map. }