diff --git a/Core/GameEngine/Include/Common/FileSystem.h b/Core/GameEngine/Include/Common/FileSystem.h index 02a932f1313..5c1f8d04a8c 100644 --- a/Core/GameEngine/Include/Common/FileSystem.h +++ b/Core/GameEngine/Include/Common/FileSystem.h @@ -134,6 +134,8 @@ struct FileInfo { // TheSuperHackers @bugfix xezon 26/10/2025 Adds a mutex to the file exist map to try prevent // application hangs during level load after the file exist map was corrupted because of writes // from multiple threads. +// +// TheSuperHackers @feature Mauller 24/04/2026 Add extension removal functions //=============================== class FileSystem : public SubsystemInterface { @@ -158,6 +160,9 @@ class FileSystem : public SubsystemInterface static AsciiString normalizePath(const AsciiString& path); ///< normalizes a file path. The path can refer to a directory. File path must be absolute, but does not need to exist. Returns an empty string on failure. static Bool isPathInDirectory(const AsciiString& testPath, const AsciiString& basePath); ///< determines if a file path is within a base path. Both paths must be absolute, but do not need to exist. + static bool removeExtension(AsciiString& path); + static bool removeExtension(UnicodeString& path); + protected: #if ENABLE_FILESYSTEM_EXISTENCE_CACHE struct FileExistData diff --git a/Core/GameEngine/Include/Common/UnicodeString.h b/Core/GameEngine/Include/Common/UnicodeString.h index a6fbcab367e..0f3c2333c44 100644 --- a/Core/GameEngine/Include/Common/UnicodeString.h +++ b/Core/GameEngine/Include/Common/UnicodeString.h @@ -306,6 +306,16 @@ class UnicodeString */ int compareNoCase(const WideChar* s) const; + /** + Conceptually identical to wcschr(). + */ + const WideChar* find(WideChar c) const; + + /** + Conceptually identical to wcsrchr(). + */ + const WideChar* reverseFind(WideChar c) const; + /** return true iff self starts with the given string. */ @@ -485,6 +495,18 @@ inline int UnicodeString::compareNoCase(const WideChar* s) const return _wcsicmp(this->str(), s); } +// ----------------------------------------------------- +inline const WideChar* UnicodeString::find(WideChar c) const +{ + return wcschr(this->str(), c); +} + +// ----------------------------------------------------- +inline const WideChar* UnicodeString::reverseFind(WideChar c) const +{ + return wcsrchr(this->str(), c); +} + // ----------------------------------------------------- inline Bool operator==(const UnicodeString& s1, const UnicodeString& s2) { diff --git a/Core/GameEngine/Source/Common/System/FileSystem.cpp b/Core/GameEngine/Source/Common/System/FileSystem.cpp index e1745a31ed9..b8e4c4695b6 100644 --- a/Core/GameEngine/Source/Common/System/FileSystem.cpp +++ b/Core/GameEngine/Source/Common/System/FileSystem.cpp @@ -54,6 +54,8 @@ #include "Common/LocalFileSystem.h" #include "Common/PerfTimer.h" +#include "Lib/PathUtil.h" + DECLARE_PERF_TIMER(FileSystem) @@ -378,3 +380,31 @@ Bool FileSystem::isPathInDirectory(const AsciiString& testPath, const AsciiStrin return true; } + +//============================================================================ +// FileSystem::removeExtension - Ascii handling variant +//============================================================================ +bool FileSystem::removeExtension(AsciiString& path) +{ + if (const Char* ext = getExtension(path.str())) + { + path.truncateTo(ext - path.str()); + return true; + } + + return false; +} + +//============================================================================ +// FileSystem::removeExtension - Unicode handling variant +//============================================================================ +bool FileSystem::removeExtension(UnicodeString& path) +{ + if (const WideChar* ext = getExtension(path.str())) + { + path.truncateTo(ext - path.str()); + return true; + } + + return false; +} diff --git a/Core/Libraries/Include/Lib/BaseType.h b/Core/Libraries/Include/Lib/BaseType.h index 16135504d6e..cbe51e3e995 100644 --- a/Core/Libraries/Include/Lib/BaseType.h +++ b/Core/Libraries/Include/Lib/BaseType.h @@ -74,6 +74,40 @@ inline NUM highestBit(NUM x) return static_cast(y & ~(y >> 1)); } +template +inline PTR maxPtr(PTR x, PTR y) noexcept +{ + static_assert(std::is_pointer::value, "maxPtr is for pointer types only!"); + + if (x == nullptr) + return y; + + if (y == nullptr) + return x; + + if (x > y) + return x; + + return y; +} + +template +inline PTR minPtr(PTR x, PTR y) noexcept +{ + static_assert(std::is_pointer::value, "minPtr is for pointer types only!"); + + if (x == nullptr) + return y; + + if (y == nullptr) + return x; + + if (x < y) + return x; + + return y; +} + // TheSuperHackers @refactor JohnsterID 24/01/2026 Add lowercase min/max templates for GameEngine layer. // GameEngine code typically uses BaseType.h, but may include WWVegas headers (which define min/max in always.h). // Header guard prevents duplicate definitions. VC6's lacks std::min/std::max. diff --git a/Core/Libraries/Include/Lib/PathUtil.h b/Core/Libraries/Include/Lib/PathUtil.h new file mode 100644 index 00000000000..cf1ce769d91 --- /dev/null +++ b/Core/Libraries/Include/Lib/PathUtil.h @@ -0,0 +1,64 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +// This file contains macros and functions to help with path handling. + +#pragma once + +#include "BaseType.h" +#include + +inline const char* getExtension(const char* path) +{ + const char* lastDot = strrchr(path, '.'); + + if (!lastDot) + { + return nullptr; + } + + const char* lastSeparator = maxPtr(strrchr(path, '/'), strrchr(path, '\\')); + + // Check if the dot is contained in the filename + if (lastSeparator && lastDot < lastSeparator) + { + return nullptr; + } + + return lastDot; +} + +inline const wchar_t* getExtension(const wchar_t* path) +{ + const wchar_t* lastDot = wcsrchr(path, L'.'); + + if (!lastDot) + { + return nullptr; + } + + const wchar_t* lastSeparator = maxPtr(wcsrchr(path, L'/'), wcsrchr(path, L'\\')); + + // Check if the dot is contained in the filename + if (lastSeparator && lastDot < lastSeparator) + { + return nullptr; + } + + return lastDot; +} diff --git a/Generals/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp b/Generals/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp index 1fe09857c7d..7390ff5ed85 100644 --- a/Generals/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp +++ b/Generals/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp @@ -843,18 +843,6 @@ static AsciiString getMapLeafAndDirName(const AsciiString& in) } } -// ------------------------------------------------------------------------------------------------ -static AsciiString removeExtension(const AsciiString& in) -{ - if (const char* end = in.reverseFind('.')) - { - const char* begin = in.str(); - return AsciiString(begin, end - begin); - } - - return in; -} - // ------------------------------------------------------------------------------------------------ const char* PORTABLE_SAVE = "Save\\"; const char* PORTABLE_MAPS = "Maps\\"; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp b/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp index 82883c2dbfb..04cc701b5e1 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp @@ -843,18 +843,6 @@ static AsciiString getMapLeafAndDirName(const AsciiString& in) } } -// ------------------------------------------------------------------------------------------------ -static AsciiString removeExtension(const AsciiString& in) -{ - if (const char* end = in.reverseFind('.')) - { - const char* begin = in.str(); - return AsciiString(begin, end - begin); - } - - return in; -} - // ------------------------------------------------------------------------------------------------ const char* PORTABLE_SAVE = "Save\\"; const char* PORTABLE_MAPS = "Maps\\";