add GameThreadPool

This commit is contained in:
savis
2026-01-05 17:20:12 +01:00
parent dbdfd57c41
commit 7718c65d7a
12 changed files with 575 additions and 285 deletions

View File

@@ -32,6 +32,10 @@ void CActorInstance::SetMaterialAlpha(DWORD dwAlpha)
void CActorInstance::OnRender()
{
// Early out if race data is not loaded yet (async loading)
if (!m_pkCurRaceData)
return;
// MR-5: Fix effect rendering when actor is semi-transparent
// Credits to d1str4ught
if (GetAlphaValue() < 1.0f)

View File

@@ -5,6 +5,7 @@
#include "StdAfx.h"
#include "EterLib/ResourceManager.h"
#include "EterLib/GameThreadPool.h"
#include "AreaLoaderThread.h"
#include "AreaTerrain.h"
@@ -14,213 +15,112 @@
// Construction/Destruction
//////////////////////////////////////////////////////////////////////
TEMP_CAreaLoaderThread::TEMP_CAreaLoaderThread() : m_bShutdowned(false), m_pArg(NULL), m_hThread(NULL), m_uThreadID(0)
TEMP_CAreaLoaderThread::TEMP_CAreaLoaderThread() : m_bShutdowned(false)
{
}
TEMP_CAreaLoaderThread::~TEMP_CAreaLoaderThread()
{
Destroy();
Shutdown();
}
bool TEMP_CAreaLoaderThread::Create(void * arg)
{
Arg(arg);
m_hThread = (HANDLE) _beginthreadex(NULL, 0, EntryPoint, this, 0, &m_uThreadID);
if (!m_hThread)
return false;
SetThreadPriority(m_hThread, THREAD_PRIORITY_NORMAL);
// Modern implementation doesn't need explicit thread creation
// The global CGameThreadPool handles threading
m_bShutdowned = false;
return true;
}
UINT TEMP_CAreaLoaderThread::Run(void * arg)
{
if (!Setup())
return 0;
return (Execute(arg));
}
/* Static */
UINT CALLBACK TEMP_CAreaLoaderThread::EntryPoint(void * pThis)
{
TEMP_CAreaLoaderThread * pThread = (TEMP_CAreaLoaderThread *) pThis;
return pThread->Run(pThread->Arg());
}
//////////////////////////////////////////////////////////////////////////
void TEMP_CAreaLoaderThread::Destroy()
{
if (m_hSemaphore)
{
CloseHandle(m_hSemaphore);
m_hSemaphore = NULL;
}
/*
while(!m_pTerrainRequestDeque.empty())
{
CTerrain * pTerrain = m_pTerrainRequestDeque.front();
delete pTerrain;
pTerrain = NULL;
m_pTerrainRequestDeque.pop_front();
}
while(!m_pTerrainCompleteDeque.empty())
{
CTerrain * pTerrain = m_pTerrainCompleteDeque.front();
delete pTerrain;
pTerrain = NULL;
m_pTerrainCompleteDeque.pop_front();
}
*/
/*stl_wipe(m_pTerrainRequestDeque);
stl_wipe(m_pTerrainCompleteDeque);
stl_wipe(m_pAreaRequestDeque);
stl_wipe(m_pAreaCompleteDeque);*/
}
UINT TEMP_CAreaLoaderThread::Setup()
{
m_hSemaphore = CreateSemaphore(NULL, // no security attributes
0, // initial count
65535, // maximum count
NULL); // unnamed semaphore
if (!m_hSemaphore)
return 0;
return 1;
}
void TEMP_CAreaLoaderThread::Shutdown()
{
if (!m_hSemaphore)
return;
BOOL bRet;
m_bShutdowned = true;
do
// Clear any pending completed items
{
bRet = ReleaseSemaphore(m_hSemaphore, 1, NULL);
}
while (!bRet);
WaitForSingleObject(m_hThread, 10000); // 쓰레드가 종료 되기를 10초 기다림
}
UINT TEMP_CAreaLoaderThread::Execute(void * pvArg)
{
bool bProcessTerrain = true;
while (!m_bShutdowned)
{
DWORD dwWaitResult;
dwWaitResult = WaitForSingleObject(m_hSemaphore, INFINITE);
if (m_bShutdowned)
break;
switch (dwWaitResult)
{
case WAIT_OBJECT_0:
if (bProcessTerrain)
ProcessTerrain();
else
ProcessArea();
break;
case WAIT_TIMEOUT:
TraceError("TEMP_CAreaLoaderThread::Execute: Timeout occured while time-out interval is INIFITE");
break;
}
std::lock_guard<std::mutex> lock(m_TerrainCompleteMutex);
m_pTerrainCompleteDeque.clear();
}
Destroy();
return 1;
{
std::lock_guard<std::mutex> lock(m_AreaCompleteMutex);
m_pAreaCompleteDeque.clear();
}
}
void TEMP_CAreaLoaderThread::Request(CTerrain * pTerrain) // called in main thread
void TEMP_CAreaLoaderThread::Request(CTerrain * pTerrain)
{
m_TerrainRequestMutex.Lock();
m_pTerrainRequestDeque.push_back(pTerrain);
m_TerrainRequestMutex.Unlock();
if (m_bShutdowned)
return;
++m_iRestSemCount;
if (!ReleaseSemaphore(m_hSemaphore, m_iRestSemCount, NULL))
TraceError("TEMP_CAreaLoaderThread::Request: ReleaseSemaphore error");
--m_iRestSemCount;
// Enqueue terrain loading to the global thread pool
CGameThreadPool* pThreadPool = CGameThreadPool::InstancePtr();
if (pThreadPool)
{
pThreadPool->Enqueue([this, pTerrain]()
{
ProcessTerrain(pTerrain);
});
}
else
{
// Fallback to synchronous loading if thread pool not available
ProcessTerrain(pTerrain);
}
}
bool TEMP_CAreaLoaderThread::Fetch(CTerrain ** ppTerrain) // called in main thread
bool TEMP_CAreaLoaderThread::Fetch(CTerrain ** ppTerrain)
{
m_TerrainCompleteMutex.Lock();
std::lock_guard<std::mutex> lock(m_TerrainCompleteMutex);
if (m_pTerrainCompleteDeque.empty())
{
m_TerrainCompleteMutex.Unlock();
return false;
}
*ppTerrain = m_pTerrainCompleteDeque.front();
m_pTerrainCompleteDeque.pop_front();
m_TerrainCompleteMutex.Unlock();
return true;
}
void TEMP_CAreaLoaderThread::Request(CArea * pArea) // called in main thread
void TEMP_CAreaLoaderThread::Request(CArea * pArea)
{
m_AreaRequestMutex.Lock();
m_pAreaRequestDeque.push_back(pArea);
m_AreaRequestMutex.Unlock();
if (m_bShutdowned)
return;
++m_iRestSemCount;
if (!ReleaseSemaphore(m_hSemaphore, m_iRestSemCount, NULL))
TraceError("TEMP_CAreaLoaderThread::Request: ReleaseSemaphore error");
--m_iRestSemCount;
// Enqueue area loading to the global thread pool
CGameThreadPool* pThreadPool = CGameThreadPool::InstancePtr();
if (pThreadPool)
{
pThreadPool->Enqueue([this, pArea]()
{
ProcessArea(pArea);
});
}
else
{
// Fallback to synchronous loading if thread pool not available
ProcessArea(pArea);
}
}
bool TEMP_CAreaLoaderThread::Fetch(CArea ** ppArea) // called in main thread
bool TEMP_CAreaLoaderThread::Fetch(CArea ** ppArea)
{
m_AreaCompleteMutex.Lock();
std::lock_guard<std::mutex> lock(m_AreaCompleteMutex);
if (m_pAreaCompleteDeque.empty())
{
m_AreaCompleteMutex.Unlock();
return false;
}
*ppArea = m_pAreaCompleteDeque.front();
m_pAreaCompleteDeque.pop_front();
m_AreaCompleteMutex.Unlock();
return true;
}
void TEMP_CAreaLoaderThread::ProcessArea() // called in loader thread
void TEMP_CAreaLoaderThread::ProcessArea(CArea * pArea)
{
m_AreaRequestMutex.Lock();
if (m_pAreaRequestDeque.empty())
{
m_AreaRequestMutex.Unlock();
if (m_bShutdowned)
return;
}
CArea * pArea = m_pAreaRequestDeque.front();
m_pAreaRequestDeque.pop_front();
Tracef("TEMP_CAreaLoaderThread::ProcessArea() RequestDeque Size : %d\n", m_pAreaRequestDeque.size());
m_AreaRequestMutex.Unlock();
DWORD dwStartTime = ELTimer_GetMSec();
@@ -238,28 +138,17 @@ void TEMP_CAreaLoaderThread::ProcessArea() // called in loader thread
Tracef("TEMP_CAreaLoaderThread::ProcessArea LoadArea : %d ms elapsed\n", ELTimer_GetMSec() - dwStartTime);
m_AreaCompleteMutex.Lock();
m_pAreaCompleteDeque.push_back(pArea);
m_AreaCompleteMutex.Unlock();
Sleep(g_iLoadingDelayTime);
// Add to completed queue
{
std::lock_guard<std::mutex> lock(m_AreaCompleteMutex);
m_pAreaCompleteDeque.push_back(pArea);
}
}
void TEMP_CAreaLoaderThread::ProcessTerrain() // called in loader thread
void TEMP_CAreaLoaderThread::ProcessTerrain(CTerrain * pTerrain)
{
m_TerrainRequestMutex.Lock();
if (m_pTerrainRequestDeque.empty())
{
m_TerrainRequestMutex.Unlock();
if (m_bShutdowned)
return;
}
CTerrain * pTerrain = m_pTerrainRequestDeque.front();
m_pTerrainRequestDeque.pop_front();
Tracef("TEMP_CAreaLoaderThread::ProcessTerrain() RequestDeque Size : %d\n", m_pTerrainRequestDeque.size());
m_TerrainRequestMutex.Unlock();
DWORD dwStartTime = ELTimer_GetMSec();
@@ -271,26 +160,24 @@ void TEMP_CAreaLoaderThread::ProcessTerrain() // called in loader thread
const std::string & c_rStrMapName = pTerrain->GetOwner()->GetName();
char filename[256];
sprintf(filename, "%s\\%06u\\AreaProperty.txt", c_rStrMapName.c_str(), dwID);
CTokenVectorMap stTokenVectorMap;
if (!LoadMultipleTextData(filename, stTokenVectorMap))
return;
Sleep(g_iLoadingDelayTime);
if (stTokenVectorMap.end() == stTokenVectorMap.find("scripttype"))
return;
if (stTokenVectorMap.end() == stTokenVectorMap.find("areaname"))
return;
const std::string & c_rstrType = stTokenVectorMap["scripttype"][0];
const std::string & c_rstrAreaName = stTokenVectorMap["areaname"][0];
if (c_rstrType != "AreaProperty")
return;
char szRawHeightFieldname[64+1];
char szWaterMapName[64+1];
char szAttrMapName[64+1];
@@ -298,7 +185,7 @@ void TEMP_CAreaLoaderThread::ProcessTerrain() // called in loader thread
char szShadowMapName[64+1];
char szMiniMapTexName[64+1];
char szSplatName[64+1];
_snprintf(szRawHeightFieldname, sizeof(szRawHeightFieldname), "%s\\%06u\\height.raw", c_rStrMapName.c_str(), dwID);
_snprintf(szSplatName, sizeof(szSplatName), "%s\\%06u\\tile.raw", c_rStrMapName.c_str(), dwID);
_snprintf(szAttrMapName, sizeof(szAttrMapName), "%s\\%06u\\attr.atr", c_rStrMapName.c_str(), dwID);
@@ -306,35 +193,26 @@ void TEMP_CAreaLoaderThread::ProcessTerrain() // called in loader thread
_snprintf(szShadowTexName, sizeof(szShadowTexName), "%s\\%06u\\shadowmap.dds", c_rStrMapName.c_str(), dwID);
_snprintf(szShadowMapName, sizeof(szShadowMapName), "%s\\%06u\\shadowmap.raw", c_rStrMapName.c_str(), dwID);
_snprintf(szMiniMapTexName, sizeof(szMiniMapTexName), "%s\\%06u\\minimap.dds", c_rStrMapName.c_str(), dwID);
pTerrain->CopySettingFromGlobalSetting();
pTerrain->LoadWaterMap(szWaterMapName);
Sleep(g_iLoadingDelayTime);
pTerrain->LoadHeightMap(szRawHeightFieldname);
Sleep(g_iLoadingDelayTime);
pTerrain->LoadAttrMap(szAttrMapName);
Sleep(g_iLoadingDelayTime);
pTerrain->RAW_LoadTileMap(szSplatName, true);
Sleep(g_iLoadingDelayTime);
pTerrain->LoadShadowTexture(szShadowTexName);
Sleep(g_iLoadingDelayTime);
pTerrain->LoadShadowMap(szShadowMapName);
Sleep(g_iLoadingDelayTime);
pTerrain->LoadMiniMapTexture(szMiniMapTexName);
Sleep(g_iLoadingDelayTime);
pTerrain->SetName(c_rstrAreaName.c_str());
Sleep(g_iLoadingDelayTime);
pTerrain->CalculateTerrainPatch();
Sleep(g_iLoadingDelayTime);
pTerrain->SetReady();
Tracef("TEMP_CAreaLoaderThread::ProcessTerrain LoadTerrain : %d ms elapsed\n", ELTimer_GetMSec() - dwStartTime);
m_TerrainCompleteMutex.Lock();
m_pTerrainCompleteDeque.push_back(pTerrain);
m_TerrainCompleteMutex.Unlock();
Sleep(g_iLoadingDelayTime);
// Add to completed queue
{
std::lock_guard<std::mutex> lock(m_TerrainCompleteMutex);
m_pTerrainCompleteDeque.push_back(pTerrain);
}
}

View File

@@ -2,19 +2,15 @@
//
//////////////////////////////////////////////////////////////////////
#if !defined(AFX_AREALOADERTHREAD_H__E43FBE42_42F4_4F0E_B9DA_D7B7C5EA0753__INCLUDED_)
#define AFX_AREALOADERTHREAD_H__E43FBE42_42F4_4F0E_B9DA_D7B7C5EA0753__INCLUDED_
#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000
#include "EterLib/Mutex.h"
#include <deque>
#include <mutex>
class CTerrain;
class CArea;
class TEMP_CAreaLoaderThread
class TEMP_CAreaLoaderThread
{
public:
TEMP_CAreaLoaderThread();
@@ -24,50 +20,21 @@ public:
void Shutdown();
void Request(CTerrain * pTerrain);
bool Fetch(CTerrain ** ppTerrian);
void Request(CArea * pArea);
bool Fetch(CArea ** ppArea);
protected:
static UINT CALLBACK EntryPoint(void * pThis);
UINT Run(void * arg);
void * Arg() const { return m_pArg; }
void Arg(void * arg) { m_pArg = arg; }
HANDLE m_hThread;
private:
void * m_pArg;
unsigned m_uThreadID;
protected:
UINT Setup();
UINT Execute(void * pvArg);
void Destroy();
void ProcessTerrain();
void ProcessArea();
void ProcessTerrain(CTerrain * pTerrain);
void ProcessArea(CArea * pArea);
private:
std::deque<CTerrain *> m_pTerrainRequestDeque;
Mutex m_TerrainRequestMutex;
std::deque<CTerrain *> m_pTerrainCompleteDeque;
Mutex m_TerrainCompleteMutex;
std::deque<CArea *> m_pAreaRequestDeque;
Mutex m_AreaRequestMutex;
std::mutex m_TerrainCompleteMutex;
std::deque<CArea *> m_pAreaCompleteDeque;
Mutex m_AreaCompleteMutex;
std::mutex m_AreaCompleteMutex;
HANDLE m_hSemaphore;
int m_iRestSemCount;
bool m_bShutdowned;
};
#endif // !defined(AFX_AREALOADERTHREAD_H__E43FBE42_42F4_4F0E_B9DA_D7B7C5EA0753__INCLUDED_)

View File

@@ -2,6 +2,7 @@
#include "RaceManager.h"
#include "RaceMotionData.h"
#include "PackLib/PackManager.h"
#include "EterLib/GameThreadPool.h"
#include <future>
#include <vector>
#include <set>
@@ -374,25 +375,61 @@ CRaceData * CRaceManager::GetSelectedRaceDataPointer()
BOOL CRaceManager::GetRaceDataPointer(DWORD dwRaceIndex, CRaceData ** ppRaceData)
{
TRaceDataIterator itor = m_RaceDataMap.find(dwRaceIndex);
if (m_RaceDataMap.end() == itor)
// Thread-safe lookup
{
CRaceData* pRaceData = __LoadRaceData(dwRaceIndex);
std::lock_guard<std::mutex> lock(m_RaceDataMapMutex);
TRaceDataIterator itor = m_RaceDataMap.find(dwRaceIndex);
if (pRaceData)
if (m_RaceDataMap.end() != itor)
{
m_RaceDataMap.insert(TRaceDataMap::value_type(dwRaceIndex, pRaceData));
*ppRaceData = pRaceData;
*ppRaceData = itor->second;
return TRUE;
}
TraceError("CRaceManager::GetRaceDataPointer: cannot load data by dwRaceIndex %lu", dwRaceIndex);
return FALSE;
}
*ppRaceData = itor->second;
return TRUE;
// Check if already loading asynchronously
{
std::lock_guard<std::mutex> lock(m_LoadingRacesMutex);
if (m_LoadingRaces.find(dwRaceIndex) != m_LoadingRaces.end())
{
// Race is being loaded asynchronously
// Wait for it to complete by loading synchronously now (needed for immediate visibility)
Tracef("CRaceManager::GetRaceDataPointer: Race %lu is loading async, switching to sync load\n", dwRaceIndex);
}
}
// Not found - load synchronously to ensure immediate availability
CRaceData* pRaceData = __LoadRaceData(dwRaceIndex);
if (pRaceData)
{
std::lock_guard<std::mutex> lock(m_RaceDataMapMutex);
// Check again in case another thread loaded it
TRaceDataIterator itor = m_RaceDataMap.find(dwRaceIndex);
if (m_RaceDataMap.end() != itor)
{
// Already loaded by another thread, use that one
delete pRaceData;
*ppRaceData = itor->second;
}
else
{
// Insert our newly loaded data
m_RaceDataMap.insert(TRaceDataMap::value_type(dwRaceIndex, pRaceData));
*ppRaceData = pRaceData;
}
// Remove from loading set if present
{
std::lock_guard<std::mutex> lock2(m_LoadingRacesMutex);
m_LoadingRaces.erase(dwRaceIndex);
}
return TRUE;
}
*ppRaceData = NULL;
return FALSE;
}
void CRaceManager::SetPathName(const char * c_szPathName)
@@ -462,27 +499,19 @@ void CRaceManager::PreloadPlayerRaceMotions()
CRaceManager& rkRaceMgr = CRaceManager::Instance();
// Phase 1: Parallel Load Race Data (MSM)
std::vector<std::future<CRaceData*>> raceLoadFutures;
for (DWORD dwRace = 0; dwRace <= 7; ++dwRace)
{
TRaceDataIterator it = rkRaceMgr.m_RaceDataMap.find(dwRace);
if (it == rkRaceMgr.m_RaceDataMap.end()) {
raceLoadFutures.push_back(std::async(std::launch::async, [&rkRaceMgr, dwRace]() {
return rkRaceMgr.__LoadRaceData(dwRace);
}));
if (it == rkRaceMgr.m_RaceDataMap.end())
{
CRaceData* pRaceData = rkRaceMgr.__LoadRaceData(dwRace);
if (pRaceData)
{
rkRaceMgr.m_RaceDataMap.insert(TRaceDataMap::value_type(pRaceData->GetRaceIndex(), pRaceData));
}
}
}
for (auto& f : raceLoadFutures) {
CRaceData* pRaceData = f.get();
if (pRaceData) {
rkRaceMgr.m_RaceDataMap.insert(TRaceDataMap::value_type(pRaceData->GetRaceIndex(), pRaceData));
}
}
// Phase 2: Parallel Load Motions
std::set<CGraphicThing*> uniqueMotions;
for (DWORD dwRace = 0; dwRace <= 7; ++dwRace)
@@ -518,28 +547,117 @@ void CRaceManager::PreloadPlayerRaceMotions()
std::vector<CGraphicThing*> motionVec(uniqueMotions.begin(), uniqueMotions.end());
size_t total = motionVec.size();
if (total > 0) {
size_t threadCount = std::thread::hardware_concurrency();
if (threadCount == 0) threadCount = 4;
size_t chunkSize = (total + threadCount - 1) / threadCount;
std::vector<std::future<void>> motionFutures;
if (total > 0)
{
CGameThreadPool* pThreadPool = CGameThreadPool::InstancePtr();
if (pThreadPool && pThreadPool->IsInitialized())
{
size_t workerCount = pThreadPool->GetWorkerCount();
size_t chunkSize = (total + workerCount - 1) / workerCount;
for (size_t i = 0; i < threadCount; ++i) {
size_t start = i * chunkSize;
size_t end = std::min(start + chunkSize, total);
if (start < end) {
motionFutures.push_back(std::async(std::launch::async, [start, end, &motionVec]() {
for (size_t k = start; k < end; ++k) {
motionVec[k]->AddReference();
}
}));
std::vector<std::future<void>> futures;
futures.reserve(workerCount);
for (size_t i = 0; i < workerCount; ++i)
{
size_t start = i * chunkSize;
size_t end = std::min(start + chunkSize, total);
if (start < end)
{
// Copy values instead of capturing by reference
futures.push_back(pThreadPool->Enqueue([start, end, motionVec]() {
for (size_t k = start; k < end; ++k)
{
motionVec[k]->AddReference();
}
}));
}
}
// Wait for all tasks to complete
for (auto& f : futures)
{
f.wait();
}
}
else
{
// Fallback to sequential if thread pool not available
for (auto* pMotion : motionVec)
{
pMotion->AddReference();
}
}
for (auto& f : motionFutures) f.get();
}
s_bPreloaded = true;
}
void CRaceManager::RequestAsyncRaceLoad(DWORD dwRaceIndex)
{
// Mark as loading
{
std::lock_guard<std::mutex> lock(m_LoadingRacesMutex);
if (m_LoadingRaces.find(dwRaceIndex) != m_LoadingRaces.end())
{
// Already loading
return;
}
m_LoadingRaces.insert(dwRaceIndex);
}
// Enqueue async load to game thread pool
CGameThreadPool* pThreadPool = CGameThreadPool::InstancePtr();
if (pThreadPool)
{
pThreadPool->Enqueue([this, dwRaceIndex]()
{
CRaceData* pRaceData = __LoadRaceData(dwRaceIndex);
if (pRaceData)
{
// Thread-safe insertion
{
std::lock_guard<std::mutex> lock(m_RaceDataMapMutex);
m_RaceDataMap.insert(TRaceDataMap::value_type(dwRaceIndex, pRaceData));
}
Tracef("CRaceManager::RequestAsyncRaceLoad: Successfully loaded race %lu asynchronously\n", dwRaceIndex);
}
else
{
TraceError("CRaceManager::RequestAsyncRaceLoad: Failed to load race %lu", dwRaceIndex);
}
// Remove from loading set
{
std::lock_guard<std::mutex> lock(m_LoadingRacesMutex);
m_LoadingRaces.erase(dwRaceIndex);
}
});
}
else
{
// Fallback to synchronous loading if thread pool not available
CRaceData* pRaceData = __LoadRaceData(dwRaceIndex);
if (pRaceData)
{
std::lock_guard<std::mutex> lock(m_RaceDataMapMutex);
m_RaceDataMap.insert(TRaceDataMap::value_type(dwRaceIndex, pRaceData));
}
// Remove from loading set
{
std::lock_guard<std::mutex> lock(m_LoadingRacesMutex);
m_LoadingRaces.erase(dwRaceIndex);
}
}
}
bool CRaceManager::IsRaceLoading(DWORD dwRaceIndex) const
{
std::lock_guard<std::mutex> lock(m_LoadingRacesMutex);
return m_LoadingRaces.find(dwRaceIndex) != m_LoadingRaces.end();
}

View File

@@ -1,6 +1,8 @@
#pragma once
#include "RaceData.h"
#include <mutex>
#include <set>
class CRaceManager : public CSingleton<CRaceManager>
{
@@ -29,6 +31,10 @@ class CRaceManager : public CSingleton<CRaceManager>
BOOL GetRaceDataPointer(DWORD dwRaceIndex, CRaceData ** ppRaceData);
// Async race loading
void RequestAsyncRaceLoad(DWORD dwRaceIndex);
bool IsRaceLoading(DWORD dwRaceIndex) const;
// Race motion preloading
static void PreloadPlayerRaceMotions();
static bool IsPreloaded() { return s_bPreloaded; }
@@ -42,6 +48,10 @@ class CRaceManager : public CSingleton<CRaceManager>
protected:
TRaceDataMap m_RaceDataMap;
mutable std::mutex m_RaceDataMapMutex;
std::set<DWORD> m_LoadingRaces;
mutable std::mutex m_LoadingRacesMutex;
std::map<std::string, std::string> m_kMap_stRaceName_stSrcName;
std::map<DWORD, std::string> m_kMap_dwRaceKey_stRaceName;