From 6461aa85d3eb11d4cee328894a1f048b23cb244b Mon Sep 17 00:00:00 2001 From: JezuzLizard Date: Wed, 29 Mar 2023 20:15:05 -0700 Subject: [PATCH] Move pathfinding related code to ai.cpp. Add a gsc.hpp header to allow creating methods and functions for GSC outside of the gsc component. Add experimental path generating code to ai.cpp. --- src/component/ai.cpp | 368 ++++++++++++++++++++++++++++++++++++++++++ src/component/gsc.cpp | 111 ------------- src/component/gsc.hpp | 21 +++ src/game/game.cpp | 49 ++++++ src/game/game.hpp | 2 + src/game/structs.hpp | 8 + src/game/symbols.hpp | 4 + 7 files changed, 452 insertions(+), 111 deletions(-) create mode 100644 src/component/ai.cpp create mode 100644 src/component/gsc.hpp diff --git a/src/component/ai.cpp b/src/component/ai.cpp new file mode 100644 index 0000000..8073f45 --- /dev/null +++ b/src/component/ai.cpp @@ -0,0 +1,368 @@ +#include +#include "loader/component_loader.hpp" + +#include "scheduler.hpp" +#include "component/gsc.hpp" + +#include +#include +#include +#include + +namespace ai +{ + namespace + { + /* + int __cdecl Path_AStarAlgorithm_CustomSearchInfo_FindPath_(path_t *pPath, team_t eTeam, const float *vStartPos, pathnode_t *pNodeFrom, const float *vGoalPos, int bAllowNegotiationLinks, CustomSearchInfo_FindPath *searchInfo, int bIncludeGoalInPath, pathnode_t *bIgnoreBadPlaces) + { + float *returnVGoaPosl; // edx + int nodeLinkIndex; // ebx + pathnode_t *returnStartNode; // ecx + pathnode_t *returnEndNode; // edi + pathlink_s *endNodeLinks; // eax + int nodeNum; // esi + float *endNodeLinkDist; // eax + pathnode_t *pSuccessor; // esi + float v17; // xmm0_4 + pathnode_t *v18; // eax + pathnode_t *v19; // eax + float v20; // xmm1_4 + long double v21; // st7 + float v22; // xmm3_4 + float v23; // xmm0_4 + float v24; // xmm1_4 + float v25; // xmm2_4 + float v26; // xmm0_4 + float v27; // xmm1_4 + float v28; // xmm1_4 + pathnode_t *pInsert; // eax + pathnode_t *v30; // ecx + pathnode_t *v31; // eax + int success; // eax + float v33; // [esp+10h] [ebp-8Ch] + int linkCount; // [esp+14h] [ebp-88h] + float negotiationOverlapCost; // [esp+18h] [ebp-84h] + pathnode_t topParent; // [esp+1Ch] [ebp-80h] BYREF + + returnVGoaPosl = (float *)vGoalPos; + nodeLinkIndex = 0; + if ( vGoalPos ) + { + g_pathAttemptGoalPos[0] = *vGoalPos; + g_pathAttemptGoalPos[1] = vGoalPos[1]; + g_pathAttemptGoalPos[2] = vGoalPos[2]; + } + else + { + g_pathAttemptGoalPos[0] = 0.0; + g_pathAttemptGoalPos[1] = 0.0; + g_pathAttemptGoalPos[2] = 0.0; + } + returnStartNode = pNodeFrom; + pNodeFrom->transient.iSearchFrame = ++level.iSearchFrame; + returnEndNode = pNodeFrom; + pNodeFrom->transient.pParent = &topParent; + pNodeFrom->transient.pNextOpen = 0; + pNodeFrom->transient.pPrevOpen = &topParent; + pNodeFrom->transient.fCost = 0.0; + topParent.transient.pNextOpen = pNodeFrom; + while ( returnEndNode != searchInfo->m_pNodeTo ) + { + topParent.transient.pNextOpen = returnEndNode->transient.pNextOpen; + if ( topParent.transient.pNextOpen ) + topParent.transient.pNextOpen->transient.pPrevOpen = &topParent; + linkCount = 0; + if ( returnEndNode->dynamic.wLinkCount > 0 ) + { + while ( 1 ) + { + if ( bIncludeGoalInPath || !returnEndNode->constant.Links[nodeLinkIndex].ubBadPlaceCount[eTeam] ) + { + endNodeLinks = returnEndNode->constant.Links; + nodeNum = endNodeLinks[nodeLinkIndex].nodeNum; + endNodeLinkDist = &endNodeLinks[nodeLinkIndex].fDist; + pSuccessor = &gameWorldCurrent->path.nodes[nodeNum]; + if ( returnEndNode->constant.type != NODE_NEGOTIATION_BEGIN || pSuccessor->constant.type != NODE_NEGOTIATION_END || !returnEndNode->dynamic.wOverlapCount && !pSuccessor->dynamic.wOverlapCount ) + { + if ( pSuccessor->transient.iSearchFrame != level.iSearchFrame ) + { + pSuccessor->transient.iSearchFrame = level.iSearchFrame; + negotiationOverlapCost = searchInfo->negotiationOverlapCost; + v20 = vGoalPos[1] - pSuccessor->constant.vOrigin[1]; + v21 = sqrtf((float)((float)(*vGoalPos - pSuccessor->constant.vOrigin[0]) * (float)(*vGoalPos - pSuccessor->constant.vOrigin[0])) + (float)(v20 * v20)); + v22 = pSuccessor->constant.minUseDistSq; + if ( v22 <= 1.0 || (v23 = searchInfo->startPos[0] - pSuccessor->constant.vOrigin[0], v24 = searchInfo->startPos[1] - pSuccessor->constant.vOrigin[1], v25 = searchInfo->startPos[2] - pSuccessor->constant.vOrigin[2], v22 <= (float)((float)((float)(v23 * v23) + (float)(v24 * v24)) + (float)(v25 * v25))) ) + { + v26 = v21 + (double)pSuccessor->dynamic.userCount * negotiationOverlapCost; + } + else + { + v33 = v21 + (double)pSuccessor->dynamic.userCount * negotiationOverlapCost; + v26 = negotiationOverlapCost + v33; + } + pSuccessor->transient.fHeuristic = v26; + v17 = returnEndNode->constant.Links[nodeLinkIndex].fDist + returnEndNode->transient.fCost; + LABEL_25: + v27 = pSuccessor->transient.fHeuristic; + pSuccessor->transient.pParent = returnEndNode; + pSuccessor->transient.fCost = v17; + v28 = v27 + v17; + pInsert = &topParent; + if ( topParent.transient.pNextOpen ) + { + do + { + v30 = pInsert->transient.pNextOpen; + if ( (float)(v30->transient.fHeuristic + v30->transient.fCost) >= v28 ) + break; + pInsert = pInsert->transient.pNextOpen; + } + while ( v30->transient.pNextOpen ); + } + pSuccessor->transient.pPrevOpen = pInsert; + pSuccessor->transient.pNextOpen = pInsert->transient.pNextOpen; + pInsert->transient.pNextOpen = pSuccessor; + v31 = pSuccessor->transient.pNextOpen; + if ( v31 ) + v31->transient.pPrevOpen = pSuccessor; + goto LABEL_30; + } + v17 = returnEndNode->transient.fCost + *endNodeLinkDist; + if ( v17 < pSuccessor->transient.fCost ) + { + v18 = pSuccessor->transient.pPrevOpen; + if ( v18 ) + { + v18->transient.pNextOpen = pSuccessor->transient.pNextOpen; + v19 = pSuccessor->transient.pNextOpen; + if ( v19 ) + v19->transient.pPrevOpen = pSuccessor->transient.pPrevOpen; + } + goto LABEL_25; + } + } + } + LABEL_30: + ++nodeLinkIndex; + if ( ++linkCount >= returnEndNode->dynamic.wLinkCount ) + { + returnVGoaPosl = (float *)vGoalPos; + returnStartNode = pNodeFrom; + break; + } + } + } + nodeLinkIndex = 0; + returnEndNode->transient.pPrevOpen = 0; + returnEndNode = topParent.transient.pNextOpen; + if ( !topParent.transient.pNextOpen ) + return 0; + } + if ( pPath ) + success = Path_GeneratePath(pPath, eTeam, vStartPos, returnVGoaPosl, returnStartNode, returnEndNode, 1, bAllowNegotiationLinks); + else + success = 1; + return success; + } + */ + + /* + //Original + int __userpurge Path_FindPathFromTo@(float *startPos@, pathnode_t *pNodeTo@, path_t *pPath, team_t eTeam, pathnode_t *pNodeFrom, float *vGoalPos, int bAllowNegotiationLinks, int bIgnoreBadplaces) + { + int v8; // xmm0_4 + CustomSearchInfo_FindPath info; // [esp+0h] [ebp-14h] BYREF + + v8 = ai_pathNegotiationOverlapCost->current.integer; + info.m_pNodeTo = pNodeTo; + LODWORD(info.negotiationOverlapCost) = v8; + info.startPos[0] = *startPos; + info.startPos[1] = startPos[1]; + info.startPos[2] = startPos[2]; + return Path_AStarAlgorithm_CustomSearchInfo_FindPath_(pPath, eTeam, startPos, pNodeFrom, vGoalPos, bAllowNegotiationLinks, &info, bIgnoreBadplaces, pNodeTo); + } + */ + + int Path_FindPathFromTo_custom(float* startPos, game::pathnode_t* pNodeTo, game::path_t* pPath, game::team_t eTeam, game::pathnode_t* pNodeFrom, float* vGoalPos, int bAllowNegotiationLinks, int bIgnoreBadplaces) + { + int overlapCost; // xmm0_4 + game::CustomSearchInfo_FindPath info = {}; // [esp+0h] [ebp-14h] BYREF + int result; + + overlapCost = (*game::ai_pathNegotiationOverlapCost)->current.integer; + info.m_pNodeTo = pNodeTo; + info.negotiationOverlapCost = overlapCost; + info.startPos[0] = startPos[0]; + info.startPos[1] = startPos[1]; + info.startPos[2] = startPos[2]; + result = game::Path_AStarAlgorithm_CustomSearchInfo_FindPath_(pPath, eTeam, startPos, pNodeFrom, vGoalPos, bAllowNegotiationLinks, &info, bIgnoreBadplaces, pNodeTo); + return result; + } + + /* + //Original + int __userpurge Path_FindPath@(path_t* pPath@, team_t eTeam@, float* vStartPos, float* vGoalPos, int bAllowNegotiationLinks) + { + pathnode_t* pNodeTo; // esi + int result; // eax + pathnode_t* pNodeFrom; // eax + int a9; // [esp+2Ch] [ebp-304h] BYREF + int a4[192]; // [esp+30h] [ebp-300h] BYREF + + pNodeTo = Path_NearestNodeNotCrossPlanes(NAN, COERCE_FLOAT(64), (int)vGoalPos, (int)a4, 192.0, 0, 0, 0, (int)&a9, 0); + if (pNodeTo && (pNodeFrom = Path_NearestNodeNotCrossPlanes(NAN, COERCE_FLOAT(64), (int)vStartPos, (int)a4, 192.0, 0, 0, 0, (int)&a9, 0)) != 0) + result = Path_FindPathFromTo(vStartPos, pNodeTo, pPath, eTeam, pNodeFrom, vGoalPos, bAllowNegotiationLinks, 0); + else + result = 0; + return result; + } + */ + + int Path_FindPath_custom(game::path_t* pPath, game::team_t eTeam, float* vStartPos, float* vGoalPos, int bAllowNegotiationLinks) + { + int result; // eax + int returnCount = 0; // [esp+2Ch] [ebp-304h] BYREF + //game::pathsort_t nodes[64] = {}; // [esp+30h] [ebp-300h] BYREF + + std::unique_ptr nodes(new game::pathsort_t[64], [](game::pathsort_t* ptr) { delete[] ptr; }); + + const float maxHeightSq = 8.0f * 8.0f; + + //printf("Path_FindPath_stub() \n"); + + game::pathnode_t* pNodeTo = game::Path_NearestNodeNotCrossPlanes(NAN, maxHeightSq, vGoalPos, nodes.get(), 192.0, 0, 0, 0, &returnCount, 0); + game::pathnode_t* pNodeFrom = game::Path_NearestNodeNotCrossPlanes(NAN, maxHeightSq, vStartPos, nodes.get(), 192.0, 0, 0, 0, &returnCount, 0); + if (pNodeTo && pNodeFrom != 0) + result = Path_FindPathFromTo_custom(vStartPos, pNodeTo, pPath, eTeam, pNodeFrom, vGoalPos, bAllowNegotiationLinks, 0); + else + result = 0; + return result; + } + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + //utils::hook::jump(0x4CF280, Path_FindPath_stub); + + gsc::method::add("getlinkednodes", [](game::scr_entref_s ent) + { + if (ent.classnum != game::CLASS_NUM_PATHNODE) + { + game::Scr_Error("Not a pathnode", game::SCRIPTINSTANCE_SERVER, false); + return; + } + + auto primary_node = &(*game::gameWorldCurrent)->path.nodes[ent.entnum]; + + game::Scr_MakeArray(game::SCRIPTINSTANCE_SERVER); + + for (auto i = 0; i < primary_node->constant.totalLinkCount; i++) + { + auto linked_node = &(*game::gameWorldCurrent)->path.nodes[primary_node->constant.Links[i].nodeNum]; + + game::Scr_AddPathnode(game::SCRIPTINSTANCE_SERVER, linked_node); + game::Scr_AddArray(game::SCRIPTINSTANCE_SERVER); + } + }); + + gsc::method::add("getnodenumber", [](game::scr_entref_s ent) + { + if (ent.classnum != game::CLASS_NUM_PATHNODE) + { + game::Scr_Error("Not a pathnode", game::SCRIPTINSTANCE_SERVER, false); + return; + } + + auto node = &(*game::gameWorldCurrent)->path.nodes[ent.entnum]; + + auto entnum = node - (*game::gameWorldCurrent)->path.nodes; + + game::Scr_AddInt(game::SCRIPTINSTANCE_SERVER, entnum); + }); + + gsc::function::add("getnodebynumber", []() + { + auto node_num = game::Scr_GetInt(game::SCRIPTINSTANCE_SERVER, 0); + + if (node_num == game::g_path->actualNodeCount) + { + game::Scr_AddUndefined(game::SCRIPTINSTANCE_SERVER); + return; + } + + if (node_num < 0 || node_num > game::g_path->actualNodeCount) + { + game::Scr_Error(utils::string::va("Number %d is not valid for a node", node_num), game::SCRIPTINSTANCE_SERVER, false); + return; + } + + auto node = &(*game::gameWorldCurrent)->path.nodes[node_num]; + + game::Scr_AddPathnode(game::SCRIPTINSTANCE_SERVER, node); + }); + + gsc::function::add("generatepath", []() + { + auto path = std::make_unique(); + + float start_pos[3] = {}; + + float goal_pos[3] = {}; + + auto team = "neutral"s; + + auto allow_negotiation_links = false; + + game::Scr_GetVector(game::SCRIPTINSTANCE_SERVER, 0, start_pos); + game::Scr_GetVector(game::SCRIPTINSTANCE_SERVER, 1, goal_pos); + + if (game::Scr_GetNumParam(game::SCRIPTINSTANCE_SERVER) >= 3) + { + if (game::Scr_GetType(game::SCRIPTINSTANCE_SERVER, 2) != game::VAR_UNDEFINED) + { + team = game::Scr_GetString(game::SCRIPTINSTANCE_SERVER, 2); + } + + if (game::Scr_GetNumParam(game::SCRIPTINSTANCE_SERVER) >= 4) + { + allow_negotiation_links = game::Scr_GetInt(game::SCRIPTINSTANCE_SERVER, 3); + } + } + + if (!game::team_map.contains(team)) + { + game::Scr_Error(utils::string::va("Team %s is not valid", team.data()), game::SCRIPTINSTANCE_SERVER, false); + return; + } + + auto eTeam = game::team_map.at(team); + + auto success = Path_FindPath_custom(path.get(), eTeam, start_pos, goal_pos, allow_negotiation_links); + + if (!success) + { + game::Scr_AddUndefined(game::SCRIPTINSTANCE_SERVER); + return; + } + + game::Scr_MakeArray(game::SCRIPTINSTANCE_SERVER); + + //Reverse the order of the array so index 0 is from the starting point instead of the end + for (auto i = path->wPathLen; i >= 0; i--) + { + //Return the number of the node instead of the node itself because of spooky GSC VM corruption + game::Scr_AddInt(game::SCRIPTINSTANCE_SERVER, path->pts[i].iNodeNum); + game::Scr_AddArray(game::SCRIPTINSTANCE_SERVER); + } + }); + } + + private: + }; +} + +REGISTER_COMPONENT(ai::component) \ No newline at end of file diff --git a/src/component/gsc.cpp b/src/component/gsc.cpp index c62ab51..8d39d78 100644 --- a/src/component/gsc.cpp +++ b/src/component/gsc.cpp @@ -259,117 +259,6 @@ namespace gsc game::Scr_AddEntity(game::SCRIPTINSTANCE_SERVER, ent2); }); - - method::add("getlinkednodes", [](game::scr_entref_s ent) - { - if (ent.classnum != game::CLASS_NUM_PATHNODE) - { - game::Scr_Error("Not a pathnode", game::SCRIPTINSTANCE_SERVER, false); - return; - } - - auto primary_node = &(*game::gameWorldCurrent)->path.nodes[ent.entnum]; - - game::Scr_MakeArray(game::SCRIPTINSTANCE_SERVER); - - for (auto i = 0; i < primary_node->constant.totalLinkCount; i++) - { - auto linked_node = &(*game::gameWorldCurrent)->path.nodes[primary_node->constant.Links[i].nodeNum]; - - game::Scr_AddPathnode(game::SCRIPTINSTANCE_SERVER, linked_node); - game::Scr_AddArray(game::SCRIPTINSTANCE_SERVER); - } - }); - - method::add("getnodenumber", [](game::scr_entref_s ent) - { - if (ent.classnum != game::CLASS_NUM_PATHNODE) - { - game::Scr_Error("Not a pathnode", game::SCRIPTINSTANCE_SERVER, false); - return; - } - - auto node = &(*game::gameWorldCurrent)->path.nodes[ent.entnum]; - - auto entnum = node - (*game::gameWorldCurrent)->path.nodes; - - game::Scr_AddInt(game::SCRIPTINSTANCE_SERVER, entnum); - }); - - function::add("getnodebynumber", []() - { - auto node_num = game::Scr_GetInt(game::SCRIPTINSTANCE_SERVER, 0); - - if (node_num == game::g_path->actualNodeCount) - { - game::Scr_AddUndefined(game::SCRIPTINSTANCE_SERVER); - return; - } - - if (node_num < 0 || node_num > game::g_path->actualNodeCount) - { - game::Scr_Error(utils::string::va("Number %d is not valid for a node", node_num), game::SCRIPTINSTANCE_SERVER, false); - return; - } - - auto node = &(*game::gameWorldCurrent)->path.nodes[node_num]; - - game::Scr_AddPathnode(game::SCRIPTINSTANCE_SERVER, node); - }); - - function::add("generatepath", []() - { - auto path = std::make_unique(); - - float start_pos[3] = {}; - - float goal_pos[3] = {}; - - auto team = "neutral"s; - - auto allow_negotiation_links = false; - - game::Scr_GetVector(game::SCRIPTINSTANCE_SERVER, 0, start_pos); - game::Scr_GetVector(game::SCRIPTINSTANCE_SERVER, 1, goal_pos); - - if (game::Scr_GetNumParam(game::SCRIPTINSTANCE_SERVER) >= 3) - { - if (game::Scr_GetType(game::SCRIPTINSTANCE_SERVER, 2) != game::VAR_UNDEFINED) - { - team = game::Scr_GetString(game::SCRIPTINSTANCE_SERVER, 2); - } - - if (game::Scr_GetNumParam(game::SCRIPTINSTANCE_SERVER) >= 4) - { - allow_negotiation_links = game::Scr_GetInt(game::SCRIPTINSTANCE_SERVER, 3); - } - } - - if (!game::team_map.contains(team)) - { - game::Scr_Error(utils::string::va("Team %s is not valid", team.data()), game::SCRIPTINSTANCE_SERVER, false); - return; - } - - auto eTeam = game::team_map.at(team); - - auto success = game::Path_FindPath(path.get(), eTeam, start_pos, goal_pos, allow_negotiation_links); - - if (!success) - { - game::Scr_AddUndefined(game::SCRIPTINSTANCE_SERVER); - return; - } - - game::Scr_MakeArray(game::SCRIPTINSTANCE_SERVER); - - for (auto i = 0; i < path->wPathLen; i++) - { - //Return the number of the node instead of the node itself because of spooky GSC VM corruption - game::Scr_AddInt(game::SCRIPTINSTANCE_SERVER, path->pts[i].iNodeNum); - game::Scr_AddArray(game::SCRIPTINSTANCE_SERVER); - } - }); } diff --git a/src/component/gsc.hpp b/src/component/gsc.hpp new file mode 100644 index 0000000..6f080ed --- /dev/null +++ b/src/component/gsc.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include + +namespace gsc +{ + namespace + { + + } + + namespace function + { + void add(const std::string& name, const game::BuiltinFunction function); + } + + namespace method + { + void add(const std::string& name, const game::BuiltinMethod method); + } +} \ No newline at end of file diff --git a/src/game/game.cpp b/src/game/game.cpp index 4692b1b..34a821e 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -441,6 +441,55 @@ namespace game return answer; } + pathnode_t* Path_NearestNodeNotCrossPlanes(float maxDistSq, float maxHeightSq, float* vOrigin, pathsort_t* nodes, float a5, int a6, int a7, int a8, int* returnCount, int a10) + { + static const auto call_addr = SELECT(0x0, 0x55C210); + + pathnode_t* answer; + + __asm + { + push a10; + push returnCount; + push a8; + push a7; + push a6; + push a5; + push nodes; + push vOrigin; + movss xmm0, maxHeightSq; + movss xmm1, maxDistSq; + call call_addr; + add esp, 0x20; + mov answer, eax; + } + + return answer; + } + + int Path_FindPathFromTo(float* startPos, pathnode_t* pNodeTo, path_t* pPath, team_t eTeam, pathnode_t* pNodeFrom, float* vGoalPos, int bAllowNegotiationLinks, int bIgnoreBadplaces) + { + static const auto call_addr = SELECT(0x0, 0x4CF3F0); + + int answer; + + __asm + { + push bIgnoreBadplaces; + push bAllowNegotiationLinks; + push vGoalPos; + push pNodeFrom; + push eTeam; + push pPath; + mov edx, pNodeTo; + mov eax, startPos; + call call_addr; + mov answer, eax; + } + + return answer; + } + namespace plutonium { } diff --git a/src/game/game.hpp b/src/game/game.hpp index f8a0599..0c58afe 100644 --- a/src/game/game.hpp +++ b/src/game/game.hpp @@ -57,6 +57,8 @@ namespace game const char* SL_ConvertToString(scriptInstance_t inst, int id); int Path_FindPath(path_t* pPath, team_t eTeam, float* vStartPos, float* vGoalPos, int bAllowNegotiationLinks); + pathnode_t* Path_NearestNodeNotCrossPlanes(float maxDistSq, float maxHeightSq, float* vOrigin, pathsort_t* nodes, float a5, int a6, int a7, int a8, int* returnCount, int a10); + int Path_FindPathFromTo(float* startPos, pathnode_t* pNodeTo, path_t* pPath, team_t eTeam, pathnode_t* pNodeFrom, float* vGoalPos, int bAllowNegotiationLinks, int bIgnoreBadplaces); template class symbol diff --git a/src/game/structs.hpp b/src/game/structs.hpp index aecb7cb..92f7c65 100644 --- a/src/game/structs.hpp +++ b/src/game/structs.hpp @@ -1590,6 +1590,14 @@ namespace game pathlocal_t_circle circle; }; + struct CustomSearchInfo_FindPath + { + pathnode_t* m_pNodeTo; + float startPos[3]; + float negotiationOverlapCost; + }; + + enum VariableType { VAR_UNDEFINED = 0x0, diff --git a/src/game/symbols.hpp b/src/game/symbols.hpp index 0333f7d..f68ea81 100644 --- a/src/game/symbols.hpp +++ b/src/game/symbols.hpp @@ -12,6 +12,8 @@ namespace game WEAK symbol Scr_AddArray { 0x0, 0x69AA50 }; WEAK symbol SL_GetStringOfSize { 0x0, 0x68DE50 }; + WEAK symbol Path_AStarAlgorithm_CustomSearchInfo_FindPath_{ 0x0, 0x4D3190 }; + // Variables WEAK symbol cmd_functions{ 0x0, 0x1F416F4 }; @@ -25,6 +27,8 @@ namespace game WEAK symbol scrVmPub{ 0x0, 0x3BD4700 }; + WEAK symbol ai_pathNegotiationOverlapCost{ 0x0, 0x18FB224 }; + namespace plutonium { }