Skip to content

bugfix(globaldata): Fix the handling of documents folder redirection by using SHGetKnownFolderPath()#2479

Open
Mauller wants to merge 1 commit intoTheSuperHackers:mainfrom
Mauller:Mauller/fix-documents-folder-redirection
Open

bugfix(globaldata): Fix the handling of documents folder redirection by using SHGetKnownFolderPath()#2479
Mauller wants to merge 1 commit intoTheSuperHackers:mainfrom
Mauller:Mauller/fix-documents-folder-redirection

Conversation

@Mauller
Copy link

@Mauller Mauller commented Mar 20, 2026

This PR improves folder redirection handling that can occur due to Group Policy or OneDrive redirecting the users documents folder.

This only works for non retail builds as it required a function not supported under VC6 as it only appeared in Vista onwards.
SHGetKnownFolderPath() has better handling of folder redirection built into it compared to the legacy SHGetSpecialFolderPath()

Generals and zero hour use different ways to obtain the game folder name which is why the code is different between them.

@Mauller Mauller self-assigned this Mar 20, 2026
@Mauller Mauller added Bug Something is not working right, typically is user facing Controversial Is controversial Gen Relates to Generals ZH Relates to Zero Hour labels Mar 20, 2026
@greptile-apps
Copy link

greptile-apps bot commented Mar 20, 2026

Greptile Summary

This PR replaces the legacy SHGetSpecialFolderPath call (which is insensitive to OneDrive/Group Policy folder redirection) with SHGetKnownFolderPath for all compiler versions above VC6 (_MSC_VER >= 1300), improving documents-folder resolution for non-retail builds on modern systems.

  • Both changed files gate the old code behind #if defined(_MSC_VER) && _MSC_VER < 1300 and place the new SHGetKnownFolderPath path in the corresponding #else block.
  • CoTaskMemFree(pszPath) is correctly called unconditionally after the success-check block in both files (initialised to nullptr so it is a safe no-op on failure), resolving the issue raised in prior review threads.
  • The preprocessor boundary (< 1300) is consistent across both the guard and the CoTaskMemFree placement — the off-by-one edge case raised in a prior thread is no longer present.
  • All new code correctly uses nullptr instead of NULL throughout.
  • Required headers (<shlobj.h>, <windows.h>) are already included via PreRTS.h in both translation units; no additional includes are needed.
  • Minor: the new #else block in GeneralsMD/GlobalData.cpp uses tab indentation while the surrounding #if block uses 2-space indentation — purely cosmetic and does not affect correctness.

Confidence Score: 4/5

  • This PR is safe to merge; the logic is correct and the previously-identified memory-leak issues have been resolved.
  • Both files implement the same well-understood pattern correctly — pszPath is initialised to nullptr, CoTaskMemFree is unconditionally called after the success block, and the preprocessor guard is symmetrical. The only deduction is the minor tab/space indentation inconsistency in the GeneralsMD #else block, which has no runtime impact.
  • No files require special attention.

Important Files Changed

Filename Overview
Generals/Code/GameEngine/Source/Common/GlobalData.cpp Wraps legacy SHGetSpecialFolderPath in #if _MSC_VER < 1300 and adds a modern SHGetKnownFolderPath path for all other compilers. CoTaskMemFree is correctly placed outside the success-check block. Logic mirrors the existing code structure cleanly.
GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp Same preprocessor guard + SHGetKnownFolderPath pattern as the Generals counterpart. Registry-based leaf name lookup is preserved. CoTaskMemFree is placed outside the success block correctly. New block uses tab indentation while the surrounding #if block uses 2-space indentation — minor inconsistency.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[parseGameDataDefinition / GlobalData constructor] --> B{_MSC_VER < 1300?}
    B -- Yes / VC6 --> C[SHGetSpecialFolderPath\nCSIDL_PERSONAL]
    C --> D{Success?}
    D -- No --> Z[m_userDataDir stays empty]
    D -- Yes --> E[Append backslash if needed]
    E --> F[Append leaf name + backslash]
    F --> G[CreateDirectory]
    G --> H[m_userDataDir = path]

    B -- No / modern compiler --> I[SHGetKnownFolderPath\nFOLDERID_Documents]
    I --> J[CoTaskMemFree pszPath]
    I --> K{SUCCEEDED and pszPath?}
    K -- No --> J
    K -- Yes --> L[AsciiString::translate from PWSTR]
    L --> M[Append backslash if needed]
    M --> N[Append leaf name + backslash]
    N --> O[CreateDirectory]
    O --> P[m_userDataDir = path]
    P --> J
Loading

Last reviewed commit: "bugfix(globaldata): ..."

@Mauller
Copy link
Author

Mauller commented Mar 20, 2026

Ah i didn't check building with VC6, will work on this further

@Mauller Mauller marked this pull request as draft March 20, 2026 20:46
@Mauller Mauller force-pushed the Mauller/fix-documents-folder-redirection branch from 0c251b8 to 9aeb428 Compare March 20, 2026 21:05
@Mauller Mauller marked this pull request as ready for review March 20, 2026 21:07
@Mauller
Copy link
Author

Mauller commented Mar 20, 2026

Updated by making the fix conditoinal so it only works in non VC6 builds

@Mauller Mauller changed the title bugfix(globaldata): Fix the handling of documents folder redirection by using SHGetKnownFolderPath() - Vista+ required bugfix(globaldata): Fix the handling of documents folder redirection by using SHGetKnownFolderPath() Mar 20, 2026
…by using SHGetKnownFolderPath() - Vista+ required
@Mauller Mauller force-pushed the Mauller/fix-documents-folder-redirection branch from 9aeb428 to 0378670 Compare March 20, 2026 21:17
@Mauller
Copy link
Author

Mauller commented Mar 20, 2026

Cleaned up the implementations by replicating the whole code block

@Mauller Mauller removed the Controversial Is controversial label Mar 20, 2026
@Mauller
Copy link
Author

Mauller commented Mar 20, 2026

Removed controversial as this fix does not work in VC6 in the end anyway, so i had to make the fix conditional at compilation

AsciiString myDocumentsDirectory;
myDocumentsDirectory.translate(UnicodeString(pszPath));

if (myDocumentsDirectory.getCharAt(myDocumentsDirectory.getLength() -1) != '\\')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can hardcoded path separators be an issue for (future) linux support?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of this will need altering to support linux in the future, the main functions for getting the documents folder are windows API.


TheWritableGlobalData->m_userDataDir.clear();

#if defined(_MSC_VER) && _MSC_VER < 1300
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better use GetProcAddress(shell32module, "SHGetKnownFolderPath") to test the system for the function. This way the compiler condition is not required.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this work with compiling in VC6?

CreateDirectory(myDocumentsDirectory.str(), nullptr);
TheWritableGlobalData->m_userDataDir = myDocumentsDirectory;
}
CoTaskMemFree(pszPath);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest move all this into a new function to tidy up this function:

TheWritableGlobalData->m_userDataDir = BuildUserDataPath();
CreateDirectory(TheWritableGlobalData->m_userDataDir.str(), nullptr);

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it makes sense

AsciiString myDocumentsDirectory;
myDocumentsDirectory.translate(UnicodeString(pszPath));

if (myDocumentsDirectory.getCharAt(myDocumentsDirectory.getLength() -1) != '\\')
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simpler: !myDocumentsDirectory.endsWith("\\")

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, i forgot we had those functions to use now.

if (temp[strlen(temp)-1] != '\\')
strcat(temp, "\\");
strcat(temp, TheWritableGlobalData->m_userDataLeafName.str());
strcat(temp, "\\");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can probably consolidate the old string handling logic with the new logic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug Something is not working right, typically is user facing Gen Relates to Generals ZH Relates to Zero Hour

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants