Quantcast
Channel: OwnedCore - World of Warcraft Exploits, Hacks, Bots and Guides.
Viewing all articles
Browse latest Browse all 282135

Thread safety with stormlib

$
0
0
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;
}
}


Viewing all articles
Browse latest Browse all 282135

Trending Articles