diff --git a/src/client/component/gui_debug.cpp b/src/client/component/gui_debug.cpp index 80233264..d41cabdb 100644 --- a/src/client/component/gui_debug.cpp +++ b/src/client/component/gui_debug.cpp @@ -505,19 +505,19 @@ namespace gui::debug void get_pathnode_origin(game::pathnode_t* node, float* out) { - out[0] = node->vLocalOrigin[0]; - out[1] = node->vLocalOrigin[1]; - out[2] = node->vLocalOrigin[2]; + out[0] = node->constant.vLocalOrigin[0]; + out[1] = node->constant.vLocalOrigin[1]; + out[2] = node->constant.vLocalOrigin[2]; game::PathNode_WorldifyPosFromParent(node, out); } void draw_node_links(game::pathnode_t* node, float* origin) { - for (unsigned int i = 0; i < node->totalLinkCount; i++) + for (unsigned int i = 0; i < node->constant.totalLinkCount; i++) { float linked_origin[3] = {}; - const auto num = node->Links[i].nodeNum; + const auto num = node->constant.Links[i].nodeNum; const auto linked = &game::pathData->nodes[num]; get_pathnode_origin(linked, linked_origin); diff --git a/src/client/component/pathnodes.cpp b/src/client/component/pathnodes.cpp new file mode 100644 index 00000000..74bad445 --- /dev/null +++ b/src/client/component/pathnodes.cpp @@ -0,0 +1,554 @@ +#include +#include "loader/component_loader.hpp" + +#include "scheduler.hpp" +#include "command.hpp" +#include "console.hpp" + +#include "game/game.hpp" +#include "game/dvars.hpp" + +#include +#include +#include + +namespace pathnodes +{ + namespace + { + game::dvar_t* g_connect_paths; + + utils::hook::detour path_init_paths_hook; + + utils::memory::allocator allocator; + + game::pathnode_tree_t* allocate_tree() + { + ++game::pathData->nodeTreeCount; + return reinterpret_cast( + game::Hunk_AllocAlignInternal(sizeof(game::pathnode_tree_t), 4)); + } + + game::pathnode_tree_t* build_node_tree(unsigned short* node_indexes, unsigned int num_nodes) + { + if (num_nodes < 4) + { + const auto result = allocate_tree(); + result->axis = -1; + result->u.s.nodeCount = num_nodes; + result->u.s.nodes = node_indexes; + return result; + } + + game::vec2_t maxs{}; + game::vec2_t mins{}; + + const auto start_node = &game::pathData->nodes[*node_indexes]; + maxs[0] = start_node->constant.vLocalOrigin[0]; + mins[0] = maxs[0]; + maxs[1] = start_node->constant.vLocalOrigin[1]; + mins[1] = maxs[1]; + + for (auto i = 1u; i < num_nodes; i++) + { + for (auto axis = 0; axis < 2; axis++) + { + const auto node = &game::pathData->nodes[node_indexes[i]]; + const auto value = node->constant.vLocalOrigin[axis]; + if (mins[axis] <= value) + { + if (value > maxs[axis]) + { + maxs[axis] = value; + } + } + else + { + mins[axis] = value; + } + } + + } + + const auto axis = (maxs[1] - mins[1]) > (maxs[0] - mins[0]); + if ((maxs[axis] - mins[axis]) > 192.f) + { + const auto dist = (maxs[axis] + mins[axis]) * 0.5f; + auto left = 0u; + + for (auto right = num_nodes - 1; ; --right) + { + while (dist > game::pathData->nodes[node_indexes[left]].constant.vLocalOrigin[axis]) + { + ++left; + } + + while (game::pathData->nodes[node_indexes[right]].constant.vLocalOrigin[axis] > dist) + { + --right; + } + + if (left >= right) + { + break; + } + + const auto swap_node = node_indexes[left]; + node_indexes[left] = node_indexes[right]; + node_indexes[right] = swap_node; + ++left; + } + + while (2 * left < num_nodes && + game::pathData->nodes[node_indexes[left]].constant.vLocalOrigin[axis] == dist) + { + ++left; + } + + while (2 * left < num_nodes && + game::pathData->nodes[node_indexes[left - 1]].constant.vLocalOrigin[axis] == dist) + { + --left; + } + + game::pathnode_tree_t* child[2]{}; + child[0] = build_node_tree(node_indexes, left); + child[1] = build_node_tree(&node_indexes[left], num_nodes - left); + const auto result = allocate_tree(); + result->axis = axis; + result->dist = dist; + result->u.child[0] = child[0]; + result->u.child[1] = child[1]; + return result; + } + else + { + const auto result = allocate_tree(); + result->axis = -1; + result->u.s.nodeCount = num_nodes; + result->u.s.nodes = node_indexes; + return result; + } + } + + bool is_negotation_link(game::pathnode_t* from, game::pathnode_t* to) + { + return from->constant.type == game::NODE_NEGOTIATION_BEGIN && + to->constant.type == game::NODE_NEGOTIATION_END && + from->constant.target == to->constant.targetname; + } + + float vec2_normalize(float* v) + { + const auto len = std::sqrt(v[0] * v[0] + v[1] * v[1]); + + v[0] /= len; + v[1] /= len; + + return len; + } + + float vec2_length(const float* v) + { + return std::sqrt(v[0] * v[0] + v[1] * v[1]); + } + + float vec3_normalize(float* v) + { + const auto len = std::sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]); + + v[0] /= len; + v[1] /= len; + v[2] /= len; + + return len; + } + + void vector_scale(float* v, const int size, const float scale) + { + for (auto i = 0; i < size; i++) + { + v[i] *= scale; + } + } + + bool is_deflection_ok(const float* start, const float* origin, const float* move_dir) + { + game::vec2_t deflection{}; + deflection[0] = origin[0] - start[0]; + deflection[1] = origin[1] - start[1]; + const auto value = ((deflection[0] * move_dir[0]) + (deflection[1] * move_dir[1])) * -1.f; + const auto d = ( + ((value * move_dir[0]) + deflection[0]) * ((value * move_dir[0]) + deflection[0]) + + ((value * move_dir[1]) + deflection[1]) * ((value * move_dir[1]) + deflection[1]) + ); + return 0.3f > d; + } + + void vector_copy(const float* a, float* b, const int size) + { + for (auto i = 0; i < size; i++) + { + b[i] = a[i]; + } + } + + bool vector_cmp(const float* a, const float* b, const int size) + { + for (auto i = 0; i < size; i++) + { + if (a[i] != b[i]) + { + return false; + } + } + + return true; + } + + float distance_squared(const float* a, const float* b) + { + return ((a[0] - b[0]) * (a[0] - b[0]) + (a[1] - b[1]) * (a[1] - b[1])); + } + + void actor_physics(game::pmove_t* pm, game::pml_t* pml) + { + pml->previous_velocity[0] = pm->ps->velocity[0]; + pml->previous_velocity[1] = pm->ps->velocity[1]; + pml->previous_velocity[2] = pm->ps->velocity[2]; + + pml->previous_origin[0] = pm->ps->origin[0]; + pml->previous_origin[1] = pm->ps->origin[1]; + pml->previous_origin[2] = pm->ps->origin[2]; + + pml->groundTrace.hitId = 3998; + + game::PM_GroundTrace(pm, pml); + if (pml->walking) + { + game::PM_WalkMove(pm, pml); + } + else + { + game::PM_AirMove(pm, pml); + } + } + + void vector_cross(const float* a, const float* b, float* out) + { + out[0] = (a[1] * b[2]) - (a[2] * b[1]); + out[1] = (a[0] * b[2]) - (a[2] * b[0]); + out[2] = (a[0] * b[1]) - (a[1] * b[0]); + } + + bool phys_trace_passed(const float* from, const float* to, float* dist) + { + game::pml_t pml{}; + game::pmove_t pm{}; + + pm.tracemask = 0x281C011; + pm.bounds = *reinterpret_cast(0x140984950); + + pm.ps = reinterpret_cast(&game::g_entities[0].client); + pm.ps->origin[0] = from[0]; + pm.ps->origin[1] = from[1]; + pm.ps->origin[2] = from[2]; + pm.ps->gravity = 800; + + pm.ps->velocity[0] = 0.f; + pm.ps->velocity[1] = 0.f; + pm.ps->velocity[2] = 0.f; + + pml.previous_origin[0] = from[0]; + pml.previous_origin[1] = from[1]; + pml.previous_origin[2] = from[2]; + + pml.msec = 50; + pml.frametime = static_cast(pml.msec) * 0.001f; + + game::vec3_t move_dir{}; + move_dir[0] = to[0] - from[0]; + move_dir[1] = to[1] - from[1]; + + game::Vec2Normalize(move_dir); + + pml.forward[0] = move_dir[0]; + pml.forward[1] = move_dir[1]; + pml.forward[2] = move_dir[2]; + + pml.up[0] = 0.f; + pml.up[1] = 0.f; + pml.up[2] = 1.f; + + vector_cross(pml.forward, pml.up, pml.right); + + pm.cmd.forwardmove = 127; + pm.cmd.rightmove = 0; + pm.cmd.unk_float = 1.f; + + auto dist_squared = 100000.f; + auto last_ground_plane_altitude = -3.4028235e38f; + + for (auto i = 0; i < 96; i++) + { + dist_squared = ( + ((to[0] - pm.ps->origin[0]) * (to[0] - pm.ps->origin[0])) + + ((to[1] - pm.ps->origin[1]) * (to[1] - pm.ps->origin[1])) + ); + + if (dist_squared <= 16.f) + { + break; + } + + game::vec3_t start{}; + vector_copy(pm.ps->origin, start, 3); + + actor_physics(&pm, &pml); + if (vector_cmp(start, pm.ps->origin, 3) || + distance_squared(pm.ps->origin, to) > 65536.f) + { + return false; + } + + const auto has_ground_plane = pml.groundPlane && pml.groundTrace.normal[2] >= 0.3f; + if (has_ground_plane) + { + last_ground_plane_altitude = pm.ps->origin[2]; + } + + if ((last_ground_plane_altitude - pm.ps->origin[2]) > 32.f) + { + return false; + } + + if (pml.groundTrace.hitId != 3998 && !is_deflection_ok(from, pm.ps->origin, move_dir)) + { + return false; + } + } + + if ((last_ground_plane_altitude - to[2]) > 32.f) + { + return false; + } + + *dist = std::sqrtf(dist_squared); + return dist_squared <= 16.f && std::abs(pm.ps->origin[2] - to[2]) <= 18.f; + } + + bool can_link_nodes(game::pathnode_t* from, game::pathnode_t* to, float* dist, bool* negotiation_link) + { + if (is_negotation_link(from, to)) + { + *negotiation_link = true; + *dist = 15.f; + return true; + } + else + { + game::vec3_t delta{}; + delta[0] = to->constant.vLocalOrigin[0] - from->constant.vLocalOrigin[0]; + delta[1] = to->constant.vLocalOrigin[1] - from->constant.vLocalOrigin[1]; + delta[2] = to->constant.vLocalOrigin[2] - from->constant.vLocalOrigin[2]; + + if (std::abs(delta[2]) > 128.f) + { + return false; + } + + if ((delta[0] * delta[0] + delta[1] * delta[1]) > 65536.f) + { + return false; + } + + game::vec2_t move_dir{}; + move_dir[0] = to->constant.vLocalOrigin[0] - from->constant.vLocalOrigin[0]; + move_dir[1] = to->constant.vLocalOrigin[1] - from->constant.vLocalOrigin[1]; + + *dist = game::Vec2Normalize(move_dir); + *negotiation_link = false; + + return phys_trace_passed(from->constant.vLocalOrigin, to->constant.vLocalOrigin, dist); + } + } + + bool try_link_nodes(game::pathnode_t* from, game::pathnode_t* to, + game::pathlink_s* links, int max_links) + { + float dist{}; + bool negotiation_link{}; + + if (max_links <= 0) + { + console::error("[Connect paths] Out of available links, increase link buffer size\n"); + return false; + } + + if (!can_link_nodes(from, to, &dist, &negotiation_link)) + { + return false; + } + + const auto link = &links[from->constant.totalLinkCount++]; + link->nodeNum = static_cast(to - game::pathData->nodes); + link->fDist = dist; + link->disconnectCount = 0; + link->negotiationLink = negotiation_link; + return true; + } + + void link_pathnodes() + { + constexpr auto max_links = 0x80000; + const auto links_buffer = allocator.allocate_array(max_links); + auto total_link_count = 0; + + for (auto i = 0u; i < game::pathData->nodeCount; i++) + { + const auto node = &game::pathData->nodes[i]; + if ((node->constant.spawnflags & 1) || !node->constant.type) + { + continue; + } + + for (auto o = 0u; o < game::pathData->nodeCount; o++) + { + const auto other = &game::pathData->nodes[o]; + if (o == i || (other->constant.spawnflags & 1) || !other->constant.type) + { + continue; + } + + try_link_nodes(node, other, &links_buffer[total_link_count], max_links - total_link_count); + } + + total_link_count += node->constant.totalLinkCount; + if (node->constant.totalLinkCount == 0) + { + console::info("[Connect paths] Pathnode at (%f %f %f) has no links\n", + node->constant.vLocalOrigin[0], + node->constant.vLocalOrigin[1], + node->constant.vLocalOrigin[2] + ); + } + } + + console::info("[Connect paths] Total links: %i\n", total_link_count); + + auto accounted_links = 0; + for (auto i = 0u; i < game::pathData->nodeCount; i++) + { + if (game::pathData->nodes[i].constant.totalLinkCount) + { + game::pathData->nodes[i].constant.Links = &links_buffer[accounted_links]; + accounted_links += game::pathData->nodes[i].constant.totalLinkCount; + } + } + } + + float distance(float* a, float* b) + { + return std::sqrtf((a[0] - b[0]) * (a[0] - b[0]) + (a[1] - b[1]) * (a[1] - b[1])); + } + + void connect_paths() + { + console::info("[Connect paths] Node count: %i\n", game::pathData->nodeCount); + + auto original_link_count = 0; + for (auto i = 0u; i < game::pathData->nodeCount; i++) + { + original_link_count += game::pathData->nodes[i].constant.totalLinkCount; + } + + console::info("[Connect paths] Original link count: %i\n", original_link_count); + + for (auto i = 0u; i < game::pathData->nodeCount; i++) + { + if (game::pathData->nodes[i].constant.Links != nullptr) + { + console::warn("[Connect paths] Path nodes already linked\n"); + return; + } + } + + game::pathData->dynamicNodeGroupCount = 0; + game::pathData->visBytes = 0; + + link_pathnodes(); + + const auto node_indexes = allocator.allocate_array(game::pathData->nodeCount); + for (auto i = 0u; i < game::pathData->nodeCount; i++) + { + node_indexes[i] = static_cast(i); + } + + console::info("[Connect paths] Building pathnode trees...\n"); + game::pathData->nodeTreeCount = 0; + game::pathData->nodeTree = build_node_tree(node_indexes, game::pathData->nodeCount); + console::info("[Connect paths] Total trees: %i\n", game::pathData->nodeTreeCount); + } + + float pm_cmd_scale_walk_stub(void*, void*, void*) + { + return 1.f; + } + + char patch_bytes[2][5]{}; + + void patch_functions() + { + std::memcpy(&patch_bytes[0], reinterpret_cast(0x1406887D0), 5); + std::memcpy(&patch_bytes[1], reinterpret_cast(0x1403C8414), 5); + + utils::hook::jump(0x1406887D0, pm_cmd_scale_walk_stub); + utils::hook::nop(0x1403C8414, 5); + } + + void restore_code(const size_t ptr, char* data, const size_t size) + { + const auto ptr_ = reinterpret_cast(ptr); + DWORD old_protect; + VirtualProtect(ptr_, size, PAGE_EXECUTE_READWRITE, &old_protect); + + for (auto i = 0; i < size; i++) + { + ptr_[i] = data[i]; + } + + VirtualProtect(ptr_, size, old_protect, &old_protect); + FlushInstructionCache(GetCurrentProcess(), ptr_, size); + } + + void restore_functions() + { + restore_code(0x1406887D0, patch_bytes[0], 5); + restore_code(0x1403C8414, patch_bytes[1], 5); + } + + void path_init_paths_stub(void* a1, void* a2, void* a3, void* a4) + { + path_init_paths_hook.invoke(a1, a2, a3, a4); + + if (g_connect_paths->current.enabled) + { + patch_functions(); + const auto _0 = gsl::finally(restore_functions); + connect_paths(); + } + } + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + g_connect_paths = dvars::register_bool("g_connectPaths", false, 0, "Connect paths"); + path_init_paths_hook.create(0x140522250, path_init_paths_stub); + } + }; +} + +REGISTER_COMPONENT(pathnodes::component) diff --git a/src/client/game/structs.hpp b/src/client/game/structs.hpp index b2381ce1..1bb32712 100644 --- a/src/client/game/structs.hpp +++ b/src/client/game/structs.hpp @@ -103,41 +103,180 @@ namespace game static_assert(offsetof(gentity_s, flags) == 364); static_assert(offsetof(gentity_s, s) == 140); - struct pathlink_s + struct pathnode_yaworient_t { - char __pad0[0x4]; - unsigned __int16 nodeNum; - char __pad[0x6]; + float fLocalAngle; + float localForward[2]; }; - static_assert(sizeof(pathlink_s) == 12); + union $3936EE84564F75EDA6DCBAC77A545FC8 + { + pathnode_yaworient_t yaw_orient; + float angles[3]; + }; + + union PathNodeParentUnion + { + scr_string_t name; + unsigned short index; + }; + + enum nodeType + { + NODE_ERROR = 0x0, + NODE_PATHNODE = 0x1, + NODE_NEGOTIATION_BEGIN = 0x13, + NODE_NEGOTIATION_END = 0x14 + }; + + enum PathNodeErrorCode : std::int32_t + { + PNERR_NONE = 0x0, + PNERR_INSOLID = 0x1, + PNERR_FLOATING = 0x2, + PNERR_NOLINK = 0x3, + PNERR_DUPLICATE = 0x4, + PNERR_NOSTANCE = 0x5, + PNERR_INVALIDDOOR = 0x6, + PNERR_NOANGLES = 0x7, + PNERR_BADPLACEMENT = 0x8, + NUM_PATH_NODE_ERRORS = 0x9, + }; + + union $5F11B9753862CE791E23553F99FA1738 + { + float minUseDistSq; + PathNodeErrorCode error; + }; + + struct pathlink_s + { + float fDist; + unsigned short nodeNum; + unsigned char disconnectCount; + unsigned char negotiationLink; + unsigned char flags; + unsigned char ubBadPlaceCount[3]; + }; + + struct pathnode_constant_t + { + unsigned short type; + unsigned int spawnflags; + scr_string_t targetname; + scr_string_t script_linkName; + scr_string_t script_noteworthy; + scr_string_t target; + scr_string_t animscript; + int animscriptfunc; + float vLocalOrigin[3]; + $3936EE84564F75EDA6DCBAC77A545FC8 ___u9; + PathNodeParentUnion parent; + $5F11B9753862CE791E23553F99FA1738 ___u11; + short wOverlapNode[2]; + char __pad0[4]; + unsigned short totalLinkCount; + pathlink_s* Links; + scr_string_t unk; + char __pad1[4]; + }; + + struct SentientHandle + { + unsigned short number; + unsigned short infoIndex; + }; + + struct pathnode_dynamic_t + { + SentientHandle pOwner; + int iFreeTime; + int iValidTime[3]; + short wLinkCount; + short wOverlapCount; + short turretEntNumber; + unsigned char userCount; + unsigned char hasBadPlaceLink; + int spreadUsedTime[2]; + short flags; + short dangerousCount; + int recentUseProxTime; + }; + + union $73F238679C0419BE2C31C6559E8604FC + { + float nodeCost; + int linkIndex; + }; + + struct pathnode_t; + struct pathnode_transient_t + { + int iSearchFrame; + pathnode_t* pNextOpen; + pathnode_t* pPrevOpen; + pathnode_t* pParent; + float fCost; + float fHeuristic; + $73F238679C0419BE2C31C6559E8604FC ___u6; + }; struct pathnode_t { - unsigned __int16 type; - unsigned int spawnflags; - unsigned int targetname; - unsigned int script_linkName; - unsigned int script_noteworthy; - unsigned int target; - unsigned int animscript; - int animscriptfunc; - float vLocalOrigin[0x3]; - char __pad0[0x1C]; - unsigned __int16 totalLinkCount; - char __pad1[0x2]; - pathlink_s* Links; - char __pad2[0x68]; - }; // size = 192 + pathnode_constant_t constant; + pathnode_dynamic_t dynamic; + pathnode_transient_t transient; + }; - static_assert(sizeof(pathnode_t) == 192); + struct pathnode_tree_nodes_t + { + int nodeCount; + unsigned short* nodes; + }; + + struct pathnode_tree_t; + union pathnode_tree_info_t + { + pathnode_tree_t* child[2]; + pathnode_tree_nodes_t s; + }; + + struct pathnode_tree_t + { + int axis; + float dist; + pathnode_tree_info_t u; + }; + + struct PathDynamicNodeGroup + { + unsigned short parentIndex; + int nodeTreeCount; + pathnode_tree_t* nodeTree; + }; struct PathData { const char* name; unsigned int nodeCount; pathnode_t* nodes; - // ... + bool parentIndexResolved; + unsigned short version; + int visBytes; + unsigned char* pathVis; + int nodeTreeCount; + pathnode_tree_t* nodeTree; + int dynamicNodeGroupCount; + PathDynamicNodeGroup* dynamicNodeGroups; + int exposureBytes; + unsigned char* pathExposure; + int noPeekVisBytes; + unsigned char* pathNoPeekVis; + int zoneCount; + int zonesBytes; + unsigned char* pathZones; + int dynStatesBytes; + unsigned char* pathDynStates; }; struct GfxImage; @@ -1293,17 +1432,97 @@ namespace game const char* name; }; + + struct playerState_s + { + char __pad0[48]; + unsigned short gravity; + char __pad1[34]; + int pm_flags; + char __pad2[40]; + vec3_t origin; + vec3_t velocity; + }; + + static_assert(offsetof(playerState_s, origin) == 128); + + struct SprintState_s + { + int sprintButtonUpRequired; + int sprintDelay; + int lastSprintStart; + int lastSprintEnd; + int sprintStartMaxLength; + }; + + struct usercmd_s + { + int serverTime; + int buttons; + char __pad0[20]; + char forwardmove; + char rightmove; + char __pad1[2]; + float unk_float; + char __pad2[28]; + }; + struct pmove_t { + playerState_s* ps; + usercmd_s cmd; + usercmd_s oldcmd; + int tracemask; + int numtouch; + int touchents[32]; + Bounds bounds; + char __pad0[28]; + char handler; + char __pad1[0x180]; }; + //static_assert(sizeof(pmove_t) == 328); + static_assert(offsetof(pmove_t, handler) == 324); + static_assert(offsetof(pmove_t, bounds) == 272); + static_assert(offsetof(pmove_t, tracemask) == 136); + struct trace_t { - char __pad0[0x29]; - bool allsolid; // Confirmed in CM_PositionTestCapsuleInTriangle - bool startsolid; // Confirmed in PM_JitterPoint + float fraction; + float normal[3]; + int surfaceFlags; + int contents; + int hitType; + unsigned short hitId; + char __pad1[11]; + bool allsolid; + bool startsolid; + bool walkable; + char __pad0[8]; }; + struct pml_t + { + float forward[3]; + float right[3]; + float up[3]; + float frametime; + int msec; + int walking; + int groundPlane; + trace_t groundTrace; + float previous_origin[3]; + float previous_velocity[3]; + float wishdir[3]; + float platformUp[3]; + float impactSpeed; + int flinch; + }; + + static_assert(offsetof(pml_t, frametime) == 36); + static_assert(offsetof(pml_t, groundTrace) == 52); + static_assert(offsetof(pml_t, flinch) == 156); + enum Sys_Folder : std::int32_t { SF_ZONE = 0x0, diff --git a/src/client/game/symbols.hpp b/src/client/game/symbols.hpp index dd3a0e74..b722b21e 100644 --- a/src/client/game/symbols.hpp +++ b/src/client/game/symbols.hpp @@ -92,9 +92,14 @@ namespace game G_GivePlayerWeapon{0x14051B660}; WEAK symbol G_InitializeAmmo{0x1404C4110}; WEAK symbol G_SelectWeapon{0x14051C0D0}; + WEAK symbol G_TraceCapsule{0x1404CBFE0}; + WEAK symbol G_SightTrace{0x1404CBCA0}; WEAK symbol WorldPosToScreenPos{0x14036F310}; WEAK symbol Hunk_AllocateTempMemoryHigh{0x140614790}; + WEAK symbol Hunk_AllocAlignInternal{0x140613D80}; WEAK symbol I_CleanStr{0x140620660}; @@ -199,6 +204,12 @@ namespace game WEAK symbol PM_trace{0x14068F1D0}; + WEAK symbol PM_WalkMove{0x14068EBB0}; + WEAK symbol PM_AirMove{0x140686BF0}; + WEAK symbol PM_GroundTrace{0x140689AA0}; + + WEAK symbol Vec2Normalize{0x140611D80}; + WEAK symbol longjmp{0x14089EED0}; WEAK symbol _setjmp{0x1408EC2E0};