Thread safety with stormlib menu

Shout-Out

User Tag List

Results 1 to 1 of 1
  1. #1
    namreeb's Avatar Legendary

    Reputation
    668
    Join Date
    Sep 2008
    Posts
    1,029
    Thanks G/R
    8/222
    Trade Feedback
    0 (0%)
    Mentioned
    9 Post(s)
    Tagged
    0 Thread(s)

    Thread safety with stormlib

    Just thought I'd share this.

    For probably ten years now I've been using stormlib on Windows in a multi-threaded application. To my knowledge I have never encountered a problem. Recently when I tried the same code on Linux, I had all kinds of problems. After troubleshooting this for a while, I discovered that stormlib keeps an internal state associated with the file handles, which means it is not thread safe. I'm not sure why this was never a problem for me on Windows. I'm guessing that it has something to do with how the underlying file I/O works on Windows versus Linux.

    At any rate, I was able to fix my problem on Linux by adding a mutex to prevent multiple threads from reading from the MPQs simultaneously. This slowed down my code by 300% or so, which I did not want to live with -- which is just stubbornness on my part since my application is pre-processing and execution time doesn't really matter anyway. I asked the stormlib author if opening multiple read-only handles to the MPQs, one handle per file per thread, would work, and he said that he thought it should. I tried it, and success!

    Below is the code that resulted, which uses a thread-local singleton. It must be initialized with the proper data directory in each thread, which was easy for me to add to the function which creates my worker threads.

    MpqManager.hpp:

    Code:
    #pragma once
    
    #include  "utility/BinaryStream.hpp"
    
    #include  <string>
    #include  <vector>
    #include  <unordered_map>
    
    namespace parser
    {
    class MpqManager
    {
        private:
            using HANDLE = void *;
    
            std::vector<HANDLE> MpqHandles;
            std::unordered_map<std::string, unsigned int> Maps;
    
            void LoadMpq(const std::string &filePath);
    
        public:
            void Initialize();
            void Initialize(const std::string &wowDir);
    
            utility::BinaryStream *OpenFile(const std::string &file);
    
            unsigned int GetMapId(const std::string &name);
    };
    
    extern thread_local MpqManager sMpqManager;
    };
    MpqManager.cpp:

    Code:
    #include  "MpqManager.hpp"
    #include  "DBC.hpp"
    
    #include  "utility/Exception.hpp"
    #include  "StormLib.h"
    
    #include  <vector>
    #include  <sstream>
    #include  <cstdint>
    #include  <cctype>
    #include  <algorithm>
    #include  <unordered_map>
    #include  <experimental/filesystem>
    #include  <iostream>
    
    namespace fs = std::experimental::filesystem;
    
    namespace parser
    {
    thread_local MpqManager sMpqManager;
    
    void MpqManager::LoadMpq(const std::string &filePath)
    {
        HANDLE archive;
            
        if (!SFileOpenArchive(filePath.c_str(), 0, MPQ_OPEN_READ_ONLY, &archive))
            THROW("Could not open MPQ").ErrorCode();
    
        MpqHandles.push_back(archive);
    }
    
    void MpqManager::Initialize()
    {
        Initialize(".");
    }
    
    // TODO examine how the retail clients determine MPQ loading order
    void MpqManager::Initialize(const std::string &wowDir)
    {
        auto const wowPath = fs::path(wowDir);
    
        std::vector<fs::directory_entry> directories;
        std::vector<fs::path> files;
        std::vector<fs::path> patches;
    
        for (auto i = fs::directory_iterator(wowPath); i != fs::directory_iterator(); ++i)
        {
            if (fs::is_directory(i->status()))
            {
                directories.push_back(*i);
                continue;
            }
    
            if (!fs::is_regular_file(i->status()))
                continue;
    
            auto path = i->path().string();
            std::transform(path.begin(), path.end(), path.begin(), ::tolower);
    
            if (path.find(".mpq") == std::string::npos)
                continue;
    
            if (path.find("wow-update") == std::string::npos)
                files.push_back(i->path());
            else
                patches.push_back(i->path());
        }
    
        if (files.empty() && patches.empty())
            THROW("Found no MPQs");
    
        std::sort(files.begin(), files.end());
        std::sort(patches.begin(), patches.end());
    
        // all locale directories should have files named lcLE\locale-lcLE.mpq and lcLE\patch-lcLE*.mpq
        for (auto const &dir : directories)
        {
            auto const dirString = dir.path().filename().string();
    
            if (dirString.length() != 4)
                continue;
    
            auto const localeMpq = wowPath / dirString / ("locale-" + dirString + ".MPQ");
            auto found = false;
    
            std::vector<fs::path> localePatches;
            fs::path firstPatch;
            for (auto i = fs::directory_iterator(dir); i != fs::directory_iterator(); ++i)
            {
                if (fs::equivalent(*i, localeMpq))
                    found = true;
    
                auto const filename = i->path().filename().string();
    
                if (filename.find("patch-" + dirString + "-") != std::string::npos)
                    localePatches.push_back(i->path());
                else if (filename.find("patch-" + dirString) != std::string::npos)
                    firstPatch = i->path();
            }
    
            if (found)
            {
                files.push_back(localeMpq);
                if (!fs::is_empty(firstPatch))
                    files.push_back(firstPatch);
    
                std::sort(localePatches.begin(), localePatches.end());
                std::copy(localePatches.cbegin(), localePatches.cend(), std::back_inserter(files));
            }
        }
    
        // the current belief is that the game uses files from mpqs in reverse alphabetical order
        // this still needs to be checked.
        std::reverse(std::begin(files), std::end(files));
        std::reverse(std::begin(patches), std::end(patches));
    
        for (auto const &file : files)
            LoadMpq(file.string());
    
        for (auto const &file : patches)
            for (auto const &handle : MpqHandles)
                if (!SFileOpenPatchArchive(handle, file.string().c_str(), "base", 0))
                    THROW("Failed to apply patch").ErrorCode();
    
        DBC maps("DBFilesClient\\Map.dbc");
    
        for (auto i = 0u; i < maps.RecordCount(); ++i)
        {
            auto const map_name = maps.GetStringField(i, 1);
            std::string map_name_lower;
            std::transform(map_name.begin(), map_name.end(), std::back_inserter(map_name_lower), ::tolower);
    
            Maps[map_name_lower] = maps.GetField(i, 0);
        }
    }
    
    utility::BinaryStream *MpqManager::OpenFile(const std::string &file)
    {
        if (MpqHandles.empty())
            THROW("MpqManager not initialized");
    
        for (auto const &handle : MpqHandles)
        {
            if (!SFileHasFile(handle, file.c_str()))
                continue;
    
            HANDLE fileHandle;
            if (!SFileOpenFileEx(handle, file.c_str(), SFILE_OPEN_FROM_MPQ, &fileHandle))
                THROW("Error in SFileOpenFileEx").ErrorCode();
    
            auto const fileSize = SFileGetFileSize(fileHandle, nullptr);
    
            if (!fileSize)
                continue;
    
            std::vector<std::uint8_t> inFileData(fileSize);
    
            if (!SFileReadFile(fileHandle, &inFileData[0], static_cast<DWORD>(inFileData.size()), nullptr, nullptr))
            {
                SFileCloseFile(fileHandle);
                THROW("Error in SFileReadFile").ErrorCode();
            }
    
            SFileCloseFile(fileHandle);
    
            return new utility::BinaryStream(inFileData);
        }
    
        return nullptr;
    }
    
    unsigned int MpqManager::GetMapId(const std::string &name)
    {
        std::string nameLower;
        std::transform(name.begin(), name.end(), std::back_inserter(nameLower), ::tolower);
    
        auto const i = Maps.find(nameLower);
    
        if (i == Maps.end())
            THROW("Map ID for " + name + " not found");
    
        return i->second;
    }
    }

    Thread safety with stormlib
  2. Thanks Parog (1 members gave Thanks to namreeb for this useful post)

Similar Threads

  1. Premium thread issues with color text
    By thebigman in forum Trade Support
    Replies: 1
    Last Post: 10-09-2013, 07:38 AM
  2. ReadProcessMemory and thread-safety in general
    By flo8464 in forum WoW Memory Editing
    Replies: 4
    Last Post: 12-14-2009, 08:58 AM
  3. WoWMimic ELITE (Epic Thread with Freebies!)
    By Yeti in forum World of Warcraft Bots and Programs
    Replies: 105
    Last Post: 08-31-2008, 05:57 PM
  4. Thread for the bored people (with vidz, of course)
    By MaiN in forum Screenshot & Video Showoff
    Replies: 0
    Last Post: 01-05-2008, 02:32 PM
All times are GMT -5. The time now is 03:17 AM. Powered by vBulletin® Version 4.2.3
Copyright © 2025 vBulletin Solutions, Inc. All rights reserved. User Alert System provided by Advanced User Tagging (Pro) - vBulletin Mods & Addons Copyright © 2025 DragonByte Technologies Ltd.
Google Authenticator verification provided by Two-Factor Authentication (Free) - vBulletin Mods & Addons Copyright © 2025 DragonByte Technologies Ltd.
Digital Point modules: Sphinx-based search