Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Core/GameEngine/Include/Common/FileSystem.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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
Expand Down
22 changes: 22 additions & 0 deletions Core/GameEngine/Include/Common/UnicodeString.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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)
{
Expand Down
30 changes: 30 additions & 0 deletions Core/GameEngine/Source/Common/System/FileSystem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@
#include "Common/LocalFileSystem.h"
#include "Common/PerfTimer.h"

#include "Lib/PathUtil.h"


DECLARE_PERF_TIMER(FileSystem)

Expand Down Expand Up @@ -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;
}
34 changes: 34 additions & 0 deletions Core/Libraries/Include/Lib/BaseType.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,40 @@ inline NUM highestBit(NUM x)
return static_cast<NUM>(y & ~(y >> 1));
}

template <typename PTR>
inline PTR maxPtr(PTR x, PTR y) noexcept
{
static_assert(std::is_pointer<PTR>::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 <typename PTR>
inline PTR minPtr(PTR x, PTR y) noexcept
{
static_assert(std::is_pointer<PTR>::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 <algorithm> lacks std::min/std::max.
Expand Down
64 changes: 64 additions & 0 deletions Core/Libraries/Include/Lib/PathUtil.h
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

// This file contains macros and functions to help with path handling.

#pragma once

#include "BaseType.h"
#include <string.h>

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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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\\";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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\\";
Expand Down
Loading