r5sdk/r5dev/game/server/ai_networkmanager.cpp

550 lines
18 KiB
C++
Raw Normal View History

//=============================================================================//
//
// Purpose:
//
//=============================================================================//
#include "core/stdafx.h"
#include "tier0/fasttimer.h"
#include "tier1/cvar.h"
#include "tier1/cmd.h"
#include "mathlib/crc32.h"
#include "public/edict.h"
#include "filesystem/filesystem.h"
#include "game/server/ai_node.h"
#include "game/server/ai_network.h"
#include "game/server/ai_networkmanager.h"
#include <public/worldsize.h>
2022-03-22 17:18:29 +01:00
constexpr int AINET_SCRIPT_VERSION_NUMBER = 21;
constexpr int AINET_VERSION_NUMBER = 57;
constexpr int AINET_MINIMUM_SIZE = 82; // The file is at least this large when all required fields are written
constexpr const char* AINETWORK_EXT = ".ain";
constexpr const char* AINETWORK_PATH = "maps/graphs/";
/*
==============================
CAI_NetworkBuilder::BuildFile
Build AI node graph file from
in-memory structures and write
to disk to be loaded
==============================
*/
void CAI_NetworkBuilder::SaveNetworkGraph(CAI_Network* pNetwork)
{
char szMeshPath[MAX_PATH];
char szGraphPath[MAX_PATH];
V_snprintf(szMeshPath, sizeof(szMeshPath), "%s%s_%s%s", NAVMESH_PATH, g_ServerGlobalVariables->m_pszMapName, S_HULL_TYPE[E_HULL_TYPE::LARGE], NAVMESH_EXT);
V_snprintf(szGraphPath, sizeof(szGraphPath), "%s%s%s", AINETWORK_PATH, g_ServerGlobalVariables->m_pszMapName, AINETWORK_EXT);
CFastTimer masterTimer;
CFastTimer timer;
2022-03-22 17:18:29 +01:00
// Build from memory.
Msg(eDLL_T::SERVER, "++++--------------------------------------------------------------------------------------------------------------------------++++\n");
Msg(eDLL_T::SERVER, ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> AI NETWORK GRAPH FILE CONSTRUCTION STARTED <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n");
Msg(eDLL_T::SERVER, "++++--------------------------------------------------------------------------------------------------------------------------++++\n");
masterTimer.Start();
timer.Start();
FileSystem()->CreateDirHierarchy(AINETWORK_PATH, "GAME");
FileHandle_t pAIGraph = FileSystem()->Open(szGraphPath, "wb", "GAME");
if (!pAIGraph)
{
Error(eDLL_T::SERVER, NO_ERROR, "%s - Unable to write to '%s' (read-only?)\n", __FUNCTION__, szGraphPath);
return;
}
2022-03-20 17:03:46 +01:00
DevMsg(eDLL_T::SERVER, "+- Writing header...\n");
2022-03-20 17:03:46 +01:00
// Must be computed at this point.
Assert((*g_ppAINetworkManager)->IsRuntimeCRCCalculated());
2022-03-22 17:18:29 +01:00
// Large NavMesh CRC.
DevMsg(eDLL_T::SERVER, " |-- AINet version: '%d'\n", AINET_VERSION_NUMBER);
DevMsg(eDLL_T::SERVER, " |-- Map version: '%d'\n", g_ServerGlobalVariables->m_nMapVersion);
DevMsg(eDLL_T::SERVER, " |-- Runtime CRC: '0x%lX'\n", (*g_ppAINetworkManager)->GetRuntimeCRC());
2022-03-20 17:03:46 +01:00
CUtlBuffer buf;
// ---------------------------
// Save the version numbers
// ---------------------------
buf.PutInt(AINET_VERSION_NUMBER);
buf.PutInt(g_ServerGlobalVariables->m_nMapVersion);
buf.PutInt((*g_ppAINetworkManager)->GetRuntimeCRC());
2022-03-20 17:03:46 +01:00
timer.End();
Msg(eDLL_T::SERVER, "...done writing header. %lf seconds\n", timer.GetDuration().GetSeconds());
timer.Start();
2023-08-28 02:44:39 +02:00
DevMsg(eDLL_T::SERVER, "+- Writing path nodes...\n");
// -------------------------------
// Dump all the nodes to the file
// -------------------------------
buf.PutInt(pNetwork->NumPathNodes());
int totalNumLinks = 0;
for (int node = 0; node < pNetwork->NumPathNodes(); node++)
{
const CAI_Node* aiNode = pNetwork->GetPathNode(node);
2022-03-20 17:03:46 +01:00
DevMsg(eDLL_T::SERVER, " |-- Writing node '#%d' at '0x%zX'\n", aiNode->m_iID, buf.TellPut());
buf.Put(&aiNode->m_vOrigin, sizeof(Vector3D));
buf.PutFloat(aiNode->GetYaw());
buf.Put(&aiNode->m_flVOffset, sizeof(aiNode->m_flVOffset));
buf.PutChar((char)aiNode->GetType());
buf.PutInt(aiNode->GetInfo());
2022-03-20 17:03:46 +01:00
for (int j = 0; j < MAX_HULLS; j++)
{
buf.PutShort((short)aiNode->unk2[j]);
2022-03-24 02:08:27 +01:00
}
buf.Put(&aiNode->unk3, sizeof(aiNode->unk3));
buf.PutShort(aiNode->unk6);
buf.PutShort(aiNode->unk9); // Always -1;
buf.Put(&aiNode->unk11, sizeof(aiNode->unk11));
totalNumLinks += aiNode->NumLinks();
2022-03-20 17:03:46 +01:00
}
pNetwork->m_iNumLinks = totalNumLinks;
timer.End();
2023-08-28 02:44:39 +02:00
Msg(eDLL_T::SERVER, "...done writing path nodes. %lf seconds (%d nodes)\n", timer.GetDuration().GetSeconds(), pNetwork->m_iNumNodes);
timer.Start();
2023-08-28 02:44:39 +02:00
DevMsg(eDLL_T::SERVER, "+- Writing node links...\n");
DevMsg(eDLL_T::SERVER, " |-- Cached node link count: '%d'\n", totalNumLinks);
2022-03-20 17:03:46 +01:00
// -------------------------------
// Dump all the links to the file
// -------------------------------
int packedLinks = totalNumLinks / 2;
buf.PutInt(packedLinks);
2022-03-24 02:08:27 +01:00
for (int node = 0; node < pNetwork->NumPathNodes(); node++)
2022-03-20 17:03:46 +01:00
{
const CAI_Node* aiNode = pNetwork->GetPathNode(node);
for (int link = 0; link < aiNode->NumLinks(); link++)
2022-03-20 17:03:46 +01:00
{
const CAI_NodeLink* nodeLink = aiNode->GetLinkByIndex(link);
// Skip links that don't originate from current node.
if (nodeLink->m_iSrcID == aiNode->m_iID)
{
DevMsg(eDLL_T::SERVER, " |-- Writing link (%hd <--> %hd) at '0x%zX'\n", nodeLink->m_iSrcID, nodeLink->m_iDestID, buf.TellPut());
buf.PutShort(nodeLink->m_iSrcID);
buf.PutShort(nodeLink->m_iDestID);
2022-03-24 02:08:27 +01:00
buf.PutChar(nodeLink->unk1);
buf.Put(nodeLink->m_iAcceptedMoveTypes, sizeof(nodeLink->m_iAcceptedMoveTypes));
}
2022-03-20 17:03:46 +01:00
}
}
timer.End();
2023-08-28 02:44:39 +02:00
Msg(eDLL_T::SERVER, "...done writing node links. %lf seconds (%d links)\n", timer.GetDuration().GetSeconds(), pNetwork->m_iNumLinks);
timer.Start();
DevMsg(eDLL_T::SERVER, "+- Writing wc lookup table...\n");
// -------------------------------
// Dump the WC lookup table
// -------------------------------
CUtlMap<int, int> wcIDs;
SetDefLessFunc(wcIDs);
bool bCheckForProblems = false;
const CAI_NetworkEditTools* const pEditOps = (*g_ppAINetworkManager)->GetEditOps();
2022-03-22 17:18:29 +01:00
for (int node = 0; node < pNetwork->m_iNumNodes; node++)
2022-03-22 17:18:29 +01:00
{
const int nIndex = pEditOps->m_pNodeIndexTable[node];
const int iPreviousNodeBinding = wcIDs.Find(nIndex);
if (iPreviousNodeBinding != wcIDs.InvalidIndex())
{
if (!bCheckForProblems)
{
DevWarning(eDLL_T::SERVER, "******* MAP CONTAINS DUPLICATE HAMMER NODE IDS! CHECK FOR PROBLEMS IN HAMMER TO CORRECT *******\n");
bCheckForProblems = true;
}
DevWarning(eDLL_T::SERVER, " AI node %d is associated with Hammer node %d, but %d is already bound to node %d\n",
node, nIndex, nIndex, wcIDs[(unsigned short)nIndex]);
}
else
{
wcIDs.Insert(nIndex, node);
}
DevMsg(eDLL_T::SERVER, " |-- Writing Hammer node (%d <--> %d) at '0x%zX'\n", nIndex, wcIDs.Element((unsigned short)nIndex), buf.TellPut());
buf.PutInt(nIndex);
2022-03-22 17:18:29 +01:00
}
timer.End();
Msg(eDLL_T::SERVER, "...done writing wc lookup table. %lf seconds (%d indices)\n", timer.GetDuration().GetSeconds(), wcIDs.Count());
timer.Start();
DevMsg(eDLL_T::SERVER, "+- Writing traverse ex nodes...\n");
// -------------------------------
// Dump the traverse ex nodes
// -------------------------------
const int traverseExNodeCount = g_pAITraverseNodes->Count();
buf.PutShort((short)traverseExNodeCount);
FOR_EACH_VEC(*g_pAITraverseNodes, i)
{
DevMsg(eDLL_T::SERVER, " |-- Writing traverse ex node '%d' at '0x%zX'\n", i, buf.TellPut());
const CAI_TraverseNode& traverseExNode = (*g_pAITraverseNodes)[i];
buf.Put(&traverseExNode.m_Quat, sizeof(Quaternion));
buf.PutInt(traverseExNode.m_Index_MAYBE);
}
timer.End();
Msg(eDLL_T::SERVER, "...done writing traverse ex nodes. %lf seconds (%d nodes)\n", timer.GetDuration().GetSeconds(), traverseExNodeCount);
2022-03-20 17:03:46 +01:00
timer.Start();
DevMsg(eDLL_T::SERVER, "+- Writing hull data blocks...\n");
// -------------------------------
// Dump the hull data blocks
// -------------------------------
// Pointer to numZones counter, incremented up and until
// the last counter field for the hull data block.
int* countPtr = &pNetwork->m_iNumZones;
for (int i = 0; i < MAX_HULLS; i++, countPtr++)
{
const CAI_HullData& hullData = pNetwork->m_HullData[i];
const int bufferSize = sizeof(int) * hullData.unk1;
buf.PutInt(*countPtr);
buf.PutShort(hullData.m_Count);
buf.PutShort(hullData.unk1);
buf.Put(hullData.pBuffer, bufferSize);
}
2022-03-20 17:03:46 +01:00
timer.End();
Msg(eDLL_T::SERVER, "...done writing hull data blocks. %lf seconds (%d blocks)\n", timer.GetDuration().GetSeconds(), MAX_HULLS);
timer.Start();
2023-08-28 02:44:39 +02:00
DevMsg(eDLL_T::SERVER, "+- Writing path clusters...\n");
// -------------------------------
// Dump the path clusters
// -------------------------------
const int numClusters = g_pAIPathClusters->Count();
buf.PutInt(numClusters);
FOR_EACH_VEC(*g_pAIPathClusters, i)
2022-03-20 17:03:46 +01:00
{
DevMsg(eDLL_T::SERVER, " |-- Writing cluster '#%d' at '0x%zX'\n", i, buf.TellPut());
2022-03-20 17:03:46 +01:00
const CAI_Cluster* pathClusters = (*g_pAIPathClusters)[i];
buf.PutInt(pathClusters->m_nIndex);
buf.PutChar(pathClusters->unk1);
buf.PutFloat(pathClusters->GetOrigin().x);
buf.PutFloat(pathClusters->GetOrigin().y);
buf.PutFloat(pathClusters->GetOrigin().z);
2022-03-20 17:03:46 +01:00
const int unkVec0Size = pathClusters->unkVec0.Count();
buf.PutInt(unkVec0Size);
2022-03-20 17:03:46 +01:00
FOR_EACH_VEC(pathClusters->unkVec0, j)
2022-03-20 17:03:46 +01:00
{
short unkShort = static_cast<short>(pathClusters->unkVec0[j]);
buf.PutShort(unkShort);
2022-03-20 17:03:46 +01:00
}
const int unkVec1Size = pathClusters->unkVec1.Count();
buf.PutInt(unkVec1Size);
FOR_EACH_VEC(pathClusters->unkVec1, j)
2022-03-20 17:03:46 +01:00
{
short unkShort = static_cast<short>(pathClusters->unkVec1[j]);
buf.PutShort(unkShort);
2022-03-20 17:03:46 +01:00
}
buf.PutChar(pathClusters->unk5);
2022-03-20 17:03:46 +01:00
}
timer.End();
2023-08-28 02:44:39 +02:00
Msg(eDLL_T::SERVER, "...done writing path clusters. %lf seconds (%d clusters)\n", timer.GetDuration().GetSeconds(), numClusters);
timer.Start();
DevMsg(eDLL_T::SERVER, "+- Writing cluster links...\n");
// -------------------------------
// Dump the cluster links
// -------------------------------
const int numClusterLinks = g_pAIClusterLinks->Count();
buf.PutInt(numClusterLinks);
FOR_EACH_VEC(*g_pAIClusterLinks, i)
2022-03-20 17:03:46 +01:00
{
// Disk and memory structs are literally identical here so just directly write.
const CAI_ClusterLink* clusterLink = (*g_pAIClusterLinks)[i];
DevMsg(eDLL_T::SERVER, " |-- Writing link (%hd <--> %hd) at '0x%zX'\n", clusterLink->m_iSrcID, clusterLink->m_iDestID, buf.TellPut());
buf.PutShort(clusterLink->m_iSrcID);
buf.PutShort(clusterLink->m_iDestID);
buf.PutInt(clusterLink->unk2);
buf.PutChar(clusterLink->flags);
buf.PutChar(clusterLink->unkFlags4);
buf.PutChar(clusterLink->unkFlags5);
2022-03-20 17:03:46 +01:00
}
timer.End();
2023-08-28 02:44:39 +02:00
Msg(eDLL_T::SERVER, "...done writing cluster links. %lf seconds (%d links)\n", timer.GetDuration().GetSeconds(), numClusterLinks);
// This is always set to '-1'. Likely a field for maintaining compatibility.
buf.PutInt(pNetwork->unk5);
2022-03-20 17:03:46 +01:00
2022-03-22 17:18:29 +01:00
// AIN v57 and above only (not present in r1, static array in r2, pointer to dynamic array in r5).
timer.Start();
DevMsg(eDLL_T::SERVER, "+- Writing script nodes...\n");
// -------------------------------
// Dump all the script nodes
// -------------------------------
const int numScriptNodes = pNetwork->m_iNumScriptNodes;
buf.PutInt(numScriptNodes);
for (int node = 0; node < numScriptNodes; node++)
{
2022-03-22 17:18:29 +01:00
// Disk and memory structs for script nodes are identical.
DevMsg(eDLL_T::SERVER, " |-- Writing script node '#%d' at '0x%zX'\n", node, buf.TellPut());
buf.Put(&pNetwork->m_ScriptNode[node], sizeof(CAI_ScriptNode));
}
timer.End();
Msg(eDLL_T::SERVER, "...done writing script nodes. %lf seconds (%d nodes)\n", timer.GetDuration().GetSeconds(), numScriptNodes);
timer.Start();
DevMsg(eDLL_T::SERVER, "+- Writing hint data...\n");
// -------------------------------
// Dump the hint data
// -------------------------------
const int numHinst = pNetwork->m_iNumHints;
buf.PutInt(numHinst);
for (int hint = 0; hint < numHinst; hint++)
2022-03-20 17:03:46 +01:00
{
DevMsg(eDLL_T::SERVER, " |-- Writing hint data '#%d' at '0x%zX'\n", hint, buf.TellPut());
buf.PutShort(pNetwork->m_Hints[hint]);
2022-03-20 17:03:46 +01:00
}
timer.End();
Msg(eDLL_T::SERVER, "...done writing hint data. %lf seconds (%d hints)\n", timer.GetDuration().GetSeconds(), numHinst);
timer.Start();
DevMsg(eDLL_T::SERVER, "+- Calculating navmesh crc...\n");
// -------------------------------
// Dump NavMesh CRC
// -------------------------------
FileHandle_t pNavMesh = FileSystem()->Open(szMeshPath, "rb", "GAME");
uint32_t nNavMeshCRC = NULL;
if (!pNavMesh)
{
Warning(eDLL_T::SERVER, "%s - No %s NavMesh found. Unable to calculate CRC for AI Network\n",
__FUNCTION__, S_HULL_TYPE[E_HULL_TYPE::LARGE]);
}
else
{
const ssize_t nLen = FileSystem()->Size(pNavMesh);
std::unique_ptr<uint8_t[]> pBuf(new uint8_t[nLen]);
FileSystem()->Read(pBuf.get(), nLen, pNavMesh);
FileSystem()->Close(pNavMesh);
nNavMeshCRC = crc32::update(NULL, pBuf.get(), nLen);
}
// Note: the NavMesh checksum is written at the END of the file
// to maintain compatibility with r1 and r2 AIN's.
DevMsg(eDLL_T::SERVER, " |-- Writing navmesh crc '%x' at '0x%zX'\n", nNavMeshCRC, buf.TellPut());
buf.PutInt(nNavMeshCRC);
timer.End();
Msg(eDLL_T::SERVER, "...done calculating navmesh crc. %lf seconds (%x)\n", timer.GetDuration().GetSeconds(), nNavMeshCRC);
// Write the entire buffer to the disk.
FileSystem()->Write(buf.Base(), buf.TellPut(), pAIGraph);
FileSystem()->Close(pAIGraph);
masterTimer.End();
Msg(eDLL_T::SERVER, "...done writing AI node graph. %lf seconds\n", masterTimer.GetDuration().GetSeconds());
Msg(eDLL_T::SERVER, "++++--------------------------------------------------------------------------------------------------------------------------++++\n");
Msg(eDLL_T::SERVER, "++++--------------------------------------------------------------------------------------------------------------------------++++\n");
}
/*
==============================
CAI_NetworkManager::LoadNetworkGraph
Load network from the disk
and validate status
==============================
*/
void CAI_NetworkManager::LoadNetworkGraph(CAI_NetworkManager* pManager, CUtlBuffer* pBuffer, const char* szAIGraphFile)
{
bool bNavMeshAvailable = true;
char szMeshPath[MAX_PATH];
char szGraphPath[MAX_PATH];
V_snprintf(szMeshPath, sizeof(szMeshPath), "%s%s_%s%s", NAVMESH_PATH, g_ServerGlobalVariables->m_pszMapName, S_HULL_TYPE[E_HULL_TYPE::LARGE], NAVMESH_EXT);
V_snprintf(szGraphPath, sizeof(szGraphPath), "%s%s%s", AINETWORK_PATH, g_ServerGlobalVariables->m_pszMapName, AINETWORK_EXT);
int nAiNetVersion = NULL;
int nAiMapVersion = NULL;
uint32_t nAiGraphCRC = NULL; // AIN CRC from AIN file.
uint32_t nAiNavMeshCRC = NULL; // NavMesh CRC from AIN file.
uint32_t nNavMeshCRC = NULL; // NavMesh CRC from local NM file.
uint32_t nAiRuntimeCRC = pManager->GetRuntimeCRC();
FileHandle_t pNavMesh = FileSystem()->Open(szMeshPath, "rb", "GAME");
if (!pNavMesh)
{
Warning(eDLL_T::SERVER, "%s - No %s NavMesh found. Unable to calculate CRC for AI Network\n", __FUNCTION__, S_HULL_TYPE[E_HULL_TYPE::LARGE]);
bNavMeshAvailable = false;
}
else
{
const ssize_t nLen = FileSystem()->Size(pNavMesh);
std::unique_ptr<uint8_t[]> pBuf(new uint8_t[nLen]);
FileSystem()->Read(pBuf.get(), nLen, pNavMesh);
FileSystem()->Close(pNavMesh);
nNavMeshCRC = crc32::update(NULL, pBuf.get(), nLen);
}
const ssize_t nFileSize = pBuffer->TellPut();
const ssize_t nOldOffset = pBuffer->TellGet();
// Seek to the start of the buffer so we can validate the header.
pBuffer->SeekGet(CUtlBuffer::SEEK_HEAD, 0);
// If we have a NavMesh, then the minimum size is
// 'AINET_MINIMUM_SIZE' + CRC32 as the AIN needs
// a NavMesh checksum field for validation.
const int nMinimumFileSize = bNavMeshAvailable
? AINET_MINIMUM_SIZE + sizeof(nNavMeshCRC)
: AINET_MINIMUM_SIZE;
if (nFileSize >= nMinimumFileSize)
{
nAiNetVersion = pBuffer->GetInt();
nAiMapVersion = pBuffer->GetInt();
nAiGraphCRC = pBuffer->GetInt();
// Too old; build with a different game???
if (nAiNetVersion > AINET_VERSION_NUMBER)
{
Warning(eDLL_T::SERVER, "AI node graph '%s' is unsupported (net version: '%d' expected: '%d')\n",
szGraphPath, nAiNetVersion, AINET_VERSION_NUMBER);
}
// AIN file was build with a different version of the map, therefore,
// the path node positions might be invalid.
else if (nAiMapVersion != g_ServerGlobalVariables->m_nMapVersion)
{
Warning(eDLL_T::SERVER, "AI node graph '%s' is out of date (map version: '%d' expected: '%d')\n",
szGraphPath, nAiMapVersion, g_ServerGlobalVariables->m_nMapVersion);
}
// Data checksum is now what the runtime expects.
else if (nAiGraphCRC != nAiRuntimeCRC)
{
Warning(eDLL_T::SERVER, "AI node graph '%s' is out of date (ain checksum: '%x' expected: '%x')\n",
szGraphPath, nAiGraphCRC, nAiRuntimeCRC);
}
else if (bNavMeshAvailable)
{
// Seek to the end of the file, minus the size of the CRC field.
// The NavMesh CRC is written at the end of the file to maintain
// compatibility with r1 and r2 AIN files.
pBuffer->SeekGet(CUtlBuffer::SEEK_HEAD, nFileSize - sizeof(nAiNavMeshCRC));
nAiNavMeshCRC = pBuffer->GetInt();
// The AIN file was build with a different NavMesh, therefore,
// the script node positions might be incorrect.
if (nAiNavMeshCRC != nNavMeshCRC)
{
Warning(eDLL_T::SERVER, "AI node graph '%s' is out of date (nav checksum: '%x' expected: '%x')\n",
szGraphPath, nAiGraphCRC, nNavMeshCRC);
}
}
}
else
{
Error(eDLL_T::SERVER, NO_ERROR, "%s - AI node graph '%s' appears truncated\n", __FUNCTION__, szGraphPath);
}
// Recover old buffer position before we call LoadNetworkGraph.
pBuffer->SeekGet(CUtlBuffer::SEEK_HEAD, nOldOffset);
2023-08-27 11:36:58 +02:00
LoadNetworkGraphEx(pManager, pBuffer, szAIGraphFile);
}
/*
==============================
CAI_NetworkManager::LoadNetworkGraphEx
Load network
(internal)
==============================
*/
void CAI_NetworkManager::LoadNetworkGraphEx(CAI_NetworkManager* pManager, CUtlBuffer* pBuffer, const char* szAIGraphFile)
{
2023-08-27 11:36:58 +02:00
CAI_NetworkManager__LoadNetworkGraph(pManager, pBuffer, szAIGraphFile);
if (ai_ainDumpOnLoad->GetBool())
{
2023-08-27 11:36:58 +02:00
Msg(eDLL_T::SERVER, "Dumping AI Network '%s'\n", szAIGraphFile);
CAI_NetworkBuilder::SaveNetworkGraph(pManager->m_pNetwork);
}
}
/*
==============================
CAI_NetworkBuilder::Build
builds network in-memory
during level load
==============================
*/
2023-08-27 11:36:58 +02:00
void CAI_NetworkBuilder::Build(CAI_NetworkBuilder* pBuilder, CAI_Network* pAINetwork)
{
2023-08-27 11:36:58 +02:00
CAI_NetworkBuilder__Build(pBuilder, pAINetwork);
CAI_NetworkBuilder::SaveNetworkGraph(pAINetwork);
}
void VAI_NetworkManager::Detour(const bool bAttach) const
{
DetourSetup(&CAI_NetworkManager__LoadNetworkGraph, &CAI_NetworkManager::LoadNetworkGraph, bAttach);
DetourSetup(&CAI_NetworkBuilder__Build, &CAI_NetworkBuilder::Build, bAttach);
}