How to get my program to find a folder that could be anywhere on the computer?

Started by
8 comments, last by wintertime 11 years, 4 months ago

Hi guys. I'm programming a level player in Blitz Plus, and it is coming along quite nicely. In fact, in the near future, I plan on releasing the file along with a level so that people can test my game. However, I don't know what I should do about the filepaths to many files that I need to load in on different computers. On my own computer, it is easy because I know exactly where the files are. I just have to tell the computer to look at the file in "C:\Users\Nick\Desktop\LEVELDATA" and so on. However, on other computers, I have no idea where the users could be saving my files. Therefore, I will need to find the location of all of the folders. I couldn't find any functions in Blitz Plus that would help me. The closest thing I could find was a CallDLL function. Would that help at all? My other option is just to save everything within the same directory, but I would rather not do that simply for the sake of neatness. Any ideas? Thanks smile.png

I am the man who will become the pirate king!
Advertisement

Not sure how directly this translates to Blitz, but I'd suggest using the Common File Dialog API. You can use the standard Windows "File Open" or "Find Folder" dialogs. Here's a brief intro to file open: http://support.microsoft.com/kb/161286

Wielder of the Sacred Wands
[Work - ArenaNet] [Epoch Language] [Scribblings]

I recently had a similar issue, and though I ended up not using the code for open/save dialogs, I still have it.

Due to the wide-strings used by Windows, I also got these functions to convert string to wstring, which are needed by the open/save dialogs.

The other thing is that, as much as I tried, I couldn't get the defaultFolder to work - I ended up using these for a short time without setting a default folder... but they still work. You should be careful when using these, as these functions do block until the user either presses Open/Save on the dialog or Cancel.




static std::wstring s2ws(const std::string& s)
{
    int len;
    int slength = (int)s.length() + 1;
    len = MultiByteToWideChar(CP_ACP, 0, s.c_str(), slength, 0, 0); 
    std::wstring r(len, L'\0');
    MultiByteToWideChar(CP_ACP, 0, s.c_str(), slength, &r[0], len);
    return r;
}

static std::string ws2s(const std::wstring& s)
{
    int len;
    int slength = (int)s.length() + 1;
    len = WideCharToMultiByte(CP_ACP, 0, s.c_str(), slength, 0, 0, 0, 0); 
    std::string r(len, '\0');
    WideCharToMultiByte(CP_ACP, 0, s.c_str(), slength, &r[0], len, 0, 0); 
    return r;
}

static std::string OpenFileDialog (std::vector<std::string> fileTypes, std::string defaultPath)
{
    IFileOpenDialog *openDialog;
    // Create the FileOpenDialog object.
    std::string result = "";
    HRESULT hr = CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_ALL, IID_IFileOpenDialog, reinterpret_cast<void**>(&openDialog));

    // construct the array for the default file types
    COMDLG_FILTERSPEC *winFileTypes = new COMDLG_FILTERSPEC[fileTypes.size()];
    for (unsigned int i = 0; i < fileTypes.size(); i++)
    {
        std::wstring type = s2ws(fileTypes[i]);
        winFileTypes[i].pszName = type.c_str();
        winFileTypes[i].pszSpec = type.c_str();
    }
    // set the file types
    openDialog->SetFileTypes(fileTypes.size(), winFileTypes);

    // setup the default path
    IShellItem *psiFolder = nullptr;
    if (defaultPath != "")
    {
        LPCWSTR szFilePath = s2ws(defaultPath).c_str();
        HRESULT shr = SHCreateItemFromParsingName(szFilePath, NULL, IID_PPV_ARGS(&psiFolder));
            //SHCreateItemFromParsingName ( szFilePath, NULL, IID_PPV_ARGS(&psiFolder) );
        
        if (SUCCEEDED(shr))
            openDialog->SetFolder(psiFolder);
        else
            std::cout << "Error setting folder to: \"" << defaultPath << "\" - error code: " << shr;
    }


    if (SUCCEEDED(hr))
    {
        hr = openDialog->Show(NULL);
        if (SUCCEEDED(hr))
        {
            IShellItem *pItem;
            hr = openDialog->GetResult(&pItem);
            if (SUCCEEDED(hr))
            {
                PWSTR pszFilePath;
                hr = pItem->GetDisplayName(SIGDN_FILESYSPATH, &pszFilePath);

                // Display the file name to the user.
                if (SUCCEEDED(hr))
                {
                    std::wstring wstr = pszFilePath;

                    result = ws2s(wstr);
                    // ...
                    CoTaskMemFree(pszFilePath);
                }
                pItem->Release();
            }
        }
        openDialog->Release();
    }

    delete winFileTypes;
    if (psiFolder != nullptr)
        psiFolder->Release();

    return result;
}

static std::string SaveFileDialog (std::vector<std::string> fileTypes, std::string defaultPath)
{
    IFileSaveDialog *saveDialog;
    // Create the FileOpenDialog object.
    std::string result = "";
    HRESULT hr = CoCreateInstance(CLSID_FileSaveDialog, NULL, CLSCTX_ALL, IID_IFileSaveDialog, reinterpret_cast<void**>(&saveDialog));

    // construct the array for the default file types
    COMDLG_FILTERSPEC *winFileTypes = new COMDLG_FILTERSPEC[fileTypes.size()];
    for (unsigned int i = 0; i < fileTypes.size(); i++)
    {
        // set the default extension
        if (i == 0)
        {
            int fulllen = fileTypes[i].size();
            int dotloc = fileTypes[i].find_first_of('.') + 1;
            int extlen = fulllen - dotloc;
            std::wstring ext = s2ws(fileTypes[i].substr( dotloc, extlen ));
            saveDialog->SetDefaultExtension(ext.c_str());
        }
        std::wstring type = s2ws(fileTypes[i]);
        winFileTypes[i].pszName = type.c_str();
        winFileTypes[i].pszSpec = type.c_str();
    }
    // set the file types
    saveDialog->SetFileTypes(fileTypes.size(), winFileTypes);

    // setup the default path
    IShellItem *psiFolder = nullptr;
    if (defaultPath != "")
    {
        LPCWSTR szFilePath = s2ws(defaultPath).c_str();
        HRESULT shr = SHCreateItemFromParsingName(szFilePath, NULL, IID_PPV_ARGS(&psiFolder));
            //SHCreateItemFromParsingName ( szFilePath, NULL, IID_PPV_ARGS(&psiFolder) );
        
        if (SUCCEEDED(shr))
            saveDialog->SetFolder(psiFolder);
        else
            std::cout << "Error setting folder to: \"" << defaultPath << "\" - error code: " << shr;
    }


    if (SUCCEEDED(hr))
    {
        hr = saveDialog->Show(NULL);
        if (SUCCEEDED(hr))
        {
            IShellItem *pItem;
            hr = saveDialog->GetResult(&pItem);
            if (SUCCEEDED(hr))
            {
                PWSTR pszFilePath;
                hr = pItem->GetDisplayName(SIGDN_FILESYSPATH, &pszFilePath);

                // Display the file name to the user.
                if (SUCCEEDED(hr))
                {
                    std::wstring wstr = pszFilePath;

                    result = ws2s(wstr);
                    // ...
                    CoTaskMemFree(pszFilePath);
                }
                pItem->Release();
            }
        }
        saveDialog->Release();
    }

    delete winFileTypes;
    if (psiFolder != nullptr)
        psiFolder->Release();

    return result;
}

Usage of OpenFileDialog would look something like:




std::vector<std::string> filetypes;
filetypes.push_back("*.txt");   // this will cause the Open file dialog to only show .txt files to be opened
std::string myfilepath = OpenFileDialog(filetypes, "");
// use your file stream to open the path contained in myfilepath

The SaveFileDialog function works the same way:




std::vector<std::string> filetypes;
filetypes.push_back("*.txt");   // this will cause the Save file dialog to save files as txt
std::string path = SaveFileDialog(filetypes, "");
// use path to open with a file reader and write

A good thing to note is that the default SaveFileDialog behavior is to prompt the user if he wants to override the existing file (if the file they tried saving exists) - so you don't have to worry about it

Ideally your installer program / script should create a save directory and place all of your level files there. This would save the location to a settings file that you could read later on. Another option is just to have the player place the files in the same directory as the program itself. At this point assuming that your user launched the program from within the directory where it exists you can just load the files relative to the exe file. If you are using a shortcut you would want to cd to change the working directory to the correct path and then execute.

Other than those two options (and the aforementioned open file dialog) your last resort would be to iterate the file system searching for path or file names that you are looking for. This will take much longer and is completely wrong but in the worst case it could work. I'm sorry I have absolutely 0 knowledge of Blitz so I can't offer any reference or examples but at least now you have an idea of the techniques and words you might be looking for.

Dan Mayor

Professional Programmer & Hobbyist Game Developer

Seeking team for indie development opportunities, see my classifieds post

if you distribute the binary, you could create a folder that is relative to the program's exe, and simply tell people to place their levels in that folder, and simply scan that folder for all valid level files.

Check out https://www.facebook.com/LiquidGames for some great games made by me on the Playstation Mobile market.
if you distribute the binary, you could create a folder that is relative to the program's exe, and simply tell people to place their levels in that folder, and simply scan that folder for all valid level files.

Don't do this.

It won't work if users put their program in "standard" locations like Program Files, and it may break on other versions of the OS. At the very least you should use SHGetKnownFolderPath.

Wielder of the Sacred Wands
[Work - ArenaNet] [Epoch Language] [Scribblings]

if you distribute the binary, you could create a folder that is relative to the program's exe, and simply tell people to place their levels in that folder, and simply scan that folder for all valid level files.

Don't do this.

It won't work if users put their program in "standard" locations like Program Files, and it may break on other versions of the OS. At the very least you should use SHGetKnownFolderPath.

Now you got me curious. I'm currently using exactly the method described - below my exe is a folder named /data/StandaloneLevels/ which I'm currently using to read/write to.

True, I'm not actually using a relative folder, I'm using GetModuleFileName to get the running folder of the exe, and then add the /data/StandaloneLevels/ to the end. But I just went and placed it in c:/Program Files(x86)/ - and both reading and writing the levels seemed to work just fine.

if you distribute the binary, you could create a folder that is relative to the program's exe, and simply tell people to place their levels in that folder, and simply scan that folder for all valid level files.

if you distribute the binary, you could create a folder that is relative to the program's exe, and simply tell people to place their levels in that folder, and simply scan that folder for all valid level files.

Don't do this.

It won't work if users put their program in "standard" locations like Program Files, and it may break on other versions of the OS. At the very least you should use SHGetKnownFolderPath.

I was attempting to cover both of these instances here in my message. Relative to the applications executable path and relative to the current working directory are different things. Just the act of placing the file in a directory that exists in the same path as the executable is no gurantee that the level files will be relative to the executable at run time. This will only occur if the program is launched by double clicking the executable inside the directory. If you use a shortcut your current working directory is different and the relations will fail. Thus the suggestion that if your relying on this that you install the shortcuts for the user and make sure that your shortcut calls a batch file or something to change the current working directory prior to executing the application.

To quote on the Program Files path, all real users of the operating system will have read privileges to the Program Files path's and all their sub directories. Barring any writing process or any advanced PC / permissions configurations using the C:\Program Files (x86)\Your Game\Levels is no different than using C:\MyGameLevels. It would be a fairly safe assumption that if your end user is this advanced that they have gone out of their way to restrict their own access to the program files directory that they will not be installing in that location.

With all of that said, I would imagine the next point would be "The installer wants to install in the program files path!" Well if your using an installer then have the installer create a levels directory under the user's documents path, save that path to the programs settings file and do it the right way as I mentioned in the "ideal" way to handle all of this. However I think we might be over complicating the issue here, from my understanding the question is very simple (how can I find where the user put the level files). The answer to that question (without considering all the special cases that rarely apply) the answer is (make the user put it where it belongs) or (scan the entire file system and look for them).

Dan Mayor

Professional Programmer & Hobbyist Game Developer

Seeking team for indie development opportunities, see my classifieds post

Now you got me curious. I'm currently using exactly the method described - below my exe is a folder named /data/StandaloneLevels/ which I'm currently using to read/write to.

True, I'm not actually using a relative folder, I'm using GetModuleFileName to get the running folder of the exe, and then add the /data/StandaloneLevels/ to the end. But I just went and placed it in c:/Program Files(x86)/ - and both reading and writing the levels seemed to work just fine.

What Windows version and account privileges? It's well-known that limited accounts on Vista and later will run into problems, especially on installs of Windows with harsher UAC configurations. If you're on Win7 with UAC on minimal and running as an administrator, well, of course you can do that without issue ;-)

[edit] For clarity: reading is fine. Writing is where account permissions matter. If you're on an elevated account you can write freely. But it's been recommended not to do that sort of thing since the Windows 2000 era.

Wielder of the Sacred Wands
[Work - ArenaNet] [Epoch Language] [Scribblings]

It seems a bit ambiguous what leveldata is here.

If its just permanent game assets then everyone puts those into the program files subdir by his exe while installing the program.

If its extra assets like a mod, I guess many people just put those there too, though it got more and more restricted so it gets put into the users my documents folder more often these days.

Volatile data that is more like a safegame than game asset, you will probably put it in the users appdata or my documents. There should be exact guidelines on MS website.

This topic is closed to new replies.

Advertisement