diff --git a/defects/regamedll-0002/patch/regamedll-0002.patch b/defects/regamedll-0002/patch/regamedll-0002.patch new file mode 100644 index 000000000..57f123eba --- /dev/null +++ b/defects/regamedll-0002/patch/regamedll-0002.patch @@ -0,0 +1,84 @@ +# UNDF: UNDF-2026-000001006 +--- a/regamedll/dlls/hostage/hostage_localnav.h ++++ b/regamedll/dlls/hostage/hostage_localnav.h +@@ -27,6 +27,8 @@ + + #pragma once + ++#include ++ + enum PathTraversAble + { + PTRAVELS_EMPTY, +@@ -89,6 +91,14 @@ public: + static void HostagePrethink(); + static float m_flStepSize; + ++ // Hash set for (offsetX, offsetY) coordinate pairs, replacing the O(N) ++ // linear scan in NodeExists() with O(1) lookup. Reduces FindPath() ++ // from O(N^2) to O(N) where N = MAX_NODES. ++ std::unordered_set m_nodeCoordSet; ++ ++ // Pack two int offsets into a single 64-bit key for hash set lookup. ++ static int64_t PackCoord(int offsetX, int offsetY); ++ + private: + static EntityHandle m_hQueue[MAX_HOSTAGES_NAV]; + static EntityHandle m_hHostages[MAX_HOSTAGES_NAV]; +--- a/regamedll/dlls/hostage/hostage_localnav.cpp ++++ b/regamedll/dlls/hostage/hostage_localnav.cpp +@@ -62,6 +62,9 @@ node_index_t CLocalNav::AddNode(node_index_t nindexParent, Vector &vecLoc, int o + nodeNew->fSearched = FALSE; + nodeNew->nindexParent = nindexParent; + ++ // Register coordinate pair in hash set for O(1) NodeExists() lookup. ++ m_nodeCoordSet.insert(PackCoord(offsetX, offsetY)); ++ + return m_nindexAvailableNode++; + } + +@@ -71,6 +74,14 @@ localnode_t *CLocalNav::GetNode(node_index_t nindex) + return &m_nodeArr[nindex]; + } + ++// Pack two int offsets into a single 64-bit key for hash set lookup. ++// Upper 32 bits = offsetX, lower 32 bits = offsetY (cast through ++// unsigned to avoid sign-extension on negative values). ++int64_t CLocalNav::PackCoord(int offsetX, int offsetY) ++{ ++ return (static_cast(offsetX) << 32) | static_cast(offsetY); ++} ++ + node_index_t CLocalNav::NodeExists(int offsetX, int offsetY) + { +- node_index_t nindexCurrent = NODE_INVALID_EMPTY; +- localnode_t *nodeCurrent; +- +- for (nindexCurrent = m_nindexAvailableNode - 1; nindexCurrent != NODE_INVALID_EMPTY; nindexCurrent--) +- { +- nodeCurrent = GetNode(nindexCurrent); +- +- if (nodeCurrent->offsetX == offsetX && nodeCurrent->offsetY == offsetY) +- { +- break; +- } +- } +- +- return nindexCurrent; ++ int64_t key = PackCoord(offsetX, offsetY); ++ if (m_nodeCoordSet.find(key) == m_nodeCoordSet.end()) ++ return NODE_INVALID_EMPTY; ++ ++ // Caller only tests != NODE_INVALID_EMPTY; any non-negative value suffices. ++ return 0; + } + + void CLocalNav::AddPathNodes(node_index_t nindexSource, BOOL fNoMonsters) +@@ -297,6 +308,7 @@ node_index_t CLocalNav::FindPath(Vector &vecStart, Vector &vecDest, float flTarg + + m_vecStartingLoc = vecStart; + m_nindexAvailableNode = 0; ++ m_nodeCoordSet.clear(); + + AddPathNodes(NODE_INVALID_EMPTY, fNoMonsters); + nIndexBest = GetBestNode(vecStart, vecDest); diff --git a/defects/regamedll-0002/test/test_node_exists b/defects/regamedll-0002/test/test_node_exists new file mode 100755 index 000000000..ca9972f22 Binary files /dev/null and b/defects/regamedll-0002/test/test_node_exists differ diff --git a/defects/regamedll-0002/test/test_node_exists.cpp b/defects/regamedll-0002/test/test_node_exists.cpp new file mode 100644 index 000000000..059be2ecb --- /dev/null +++ b/defects/regamedll-0002/test/test_node_exists.cpp @@ -0,0 +1,198 @@ +// Unit test for regamedll-cs-0001: CLocalNav::NodeExists O(N) linear scan +// inside BFS pathfinding loop, making hostage FindPath() O(N^2). +// +// Defect: NodeExists() scans all existing nodes linearly to check if a +// coordinate pair already exists. Called 8 times per BFS expansion step +// (once per AddPathNode), with up to MAX_NODES=100 nodes, yielding +// O(100*100) = O(10,000) comparisons per FindPath() call. +// +// Fix: Replace linear scan with unordered_set keyed on packed (offsetX, +// offsetY) coordinate pairs for O(1) lookup. +// +// This test verifies correctness and measures the operation count +// reduction. + +#include +#include +#include +#include +#include +#include + +// Simulate the defect: linear scan NodeExists +static int g_unpatched_ops = 0; + +struct FakeNode { + int offsetX; + int offsetY; +}; + +static const int MAX_NODES = 100; +static FakeNode g_nodes[MAX_NODES]; +static int g_nodeCount = 0; + +// UNPATCHED: Linear scan of all nodes +int NodeExists_Unpatched(int offsetX, int offsetY) { + for (int i = g_nodeCount - 1; i >= 0; i--) { + g_unpatched_ops++; + if (g_nodes[i].offsetX == offsetX && g_nodes[i].offsetY == offsetY) { + return i; + } + } + return -1; +} + +void AddNode_Unpatched(int offsetX, int offsetY) { + if (g_nodeCount >= MAX_NODES) return; + g_nodes[g_nodeCount].offsetX = offsetX; + g_nodes[g_nodeCount].offsetY = offsetY; + g_nodeCount++; +} + +// PATCHED: Hash set lookup +static int g_patched_ops = 0; +static std::unordered_set g_nodeCoordSet; +static FakeNode g_pnodes[MAX_NODES]; +static int g_pnodeCount = 0; + +static int64_t PackCoord(int offsetX, int offsetY) { + return (static_cast(offsetX) << 32) | static_cast(offsetY); +} + +int NodeExists_Patched(int offsetX, int offsetY) { + g_patched_ops++; + int64_t key = PackCoord(offsetX, offsetY); + if (g_nodeCoordSet.find(key) == g_nodeCoordSet.end()) + return -1; + return 0; +} + +void AddNode_Patched(int offsetX, int offsetY) { + if (g_pnodeCount >= MAX_NODES) return; + g_pnodes[g_pnodeCount].offsetX = offsetX; + g_pnodes[g_pnodeCount].offsetY = offsetY; + g_pnodeCount++; + g_nodeCoordSet.insert(PackCoord(offsetX, offsetY)); +} + +// Simulate BFS expansion: add nodes in 8 directions from each explored node, +// checking NodeExists before each add (same as AddPathNode logic). +void SimulateBFS_Unpatched() { + g_nodeCount = 0; + g_unpatched_ops = 0; + + // Seed with 8 initial neighbors (like AddPathNodes(INVALID)) + int dx[] = {1, -1, 0, 0, 1, 1, -1, -1}; + int dy[] = {0, 0, 1, -1, 1, -1, 1, -1}; + + for (int d = 0; d < 8; d++) { + if (NodeExists_Unpatched(dx[d], dy[d]) == -1) { + AddNode_Unpatched(dx[d], dy[d]); + } + } + + // Expand from each node (simulating BFS loop) + for (int n = 0; n < g_nodeCount && g_nodeCount < MAX_NODES; n++) { + int bx = g_nodes[n].offsetX; + int by = g_nodes[n].offsetY; + for (int d = 0; d < 8; d++) { + int nx = bx + dx[d]; + int ny = by + dy[d]; + if (NodeExists_Unpatched(nx, ny) == -1) { + AddNode_Unpatched(nx, ny); + } + } + } +} + +void SimulateBFS_Patched() { + g_pnodeCount = 0; + g_patched_ops = 0; + g_nodeCoordSet.clear(); + + int dx[] = {1, -1, 0, 0, 1, 1, -1, -1}; + int dy[] = {0, 0, 1, -1, 1, -1, 1, -1}; + + for (int d = 0; d < 8; d++) { + if (NodeExists_Patched(dx[d], dy[d]) == -1) { + AddNode_Patched(dx[d], dy[d]); + } + } + + for (int n = 0; n < g_pnodeCount && g_pnodeCount < MAX_NODES; n++) { + int bx = g_pnodes[n].offsetX; + int by = g_pnodes[n].offsetY; + for (int d = 0; d < 8; d++) { + int nx = bx + dx[d]; + int ny = by + dy[d]; + if (NodeExists_Patched(nx, ny) == -1) { + AddNode_Patched(nx, ny); + } + } + } +} + +// Test PackCoord correctness with negative coordinates +void TestPackCoord() { + // Distinct coordinates must produce distinct keys + assert(PackCoord(0, 0) != PackCoord(1, 0)); + assert(PackCoord(0, 0) != PackCoord(0, 1)); + assert(PackCoord(-1, -1) != PackCoord(1, 1)); + assert(PackCoord(-1, 0) != PackCoord(0, -1)); + assert(PackCoord(5, -3) != PackCoord(-3, 5)); + + // Same coordinates must produce same key + assert(PackCoord(7, -2) == PackCoord(7, -2)); + assert(PackCoord(-12, 12) == PackCoord(-12, 12)); + + printf("PASS: PackCoord correctness\n"); +} + +// Test that patched version produces same node set as unpatched +void TestCorrectness() { + SimulateBFS_Unpatched(); + SimulateBFS_Patched(); + + // Both must produce exactly the same number of nodes + assert(g_nodeCount == g_pnodeCount); + + // Verify every unpatched node exists in patched set + for (int i = 0; i < g_nodeCount; i++) { + int64_t key = PackCoord(g_nodes[i].offsetX, g_nodes[i].offsetY); + assert(g_nodeCoordSet.count(key) == 1); + } + + printf("PASS: Correctness (both produce %d nodes)\n", g_nodeCount); +} + +// Test operation count reduction +void TestPerformance() { + SimulateBFS_Unpatched(); + int unpatched = g_unpatched_ops; + + SimulateBFS_Patched(); + int patched = g_patched_ops; + + double ratio = (double)unpatched / (double)patched; + + printf(" Unpatched ops: %d\n", unpatched); + printf(" Patched ops: %d\n", patched); + printf(" Ratio: %.1fx\n", ratio); + + // Unpatched should be significantly worse (at least 5x more operations) + assert(ratio >= 5.0); + + printf("PASS: Performance (%.1fx reduction)\n", ratio); +} + +int main() { + printf("regamedll-cs-0001: CLocalNav::NodeExists O(N) -> O(1) hash set\n"); + printf("Hostage pathfinding FindPath() O(N^2) -> O(N)\n\n"); + + TestPackCoord(); + TestCorrectness(); + TestPerformance(); + + printf("\nAll tests passed.\n"); + return 0; +}