From fcab3d630b1e17bdeea23ed1ed73eb42740fafad Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Tue, 31 Mar 2026 13:05:24 -0400 Subject: [PATCH] regamedll-0002: CLocalNav::NodeExists O(N) linear scan in hostage BFS, 31.1x Hostage pathfinding CLocalNav::FindPath() calls NodeExists() inside BFS expansion loop. NodeExists() linearly scans all existing nodes to check if coordinate pair already exists. With MAX_NODES=100, this is O(N^2) per FindPath() call (8 AddPathNode calls per expansion, each scanning all N nodes). Fix: unordered_set keyed on packed (offsetX, offsetY) for O(1) lookup. Reduces FindPath() from O(N^2) to O(N). 31.1x op-count reduction at N=100, 4/4 PASS. MOAD-0002 (intertangle): gpGlobals is standard GoldSrc engine state, CLEAN. MOAD-0003 (leaked context): single-threaded game DLL, no thread_local, CLEAN. MOAD-0004 (logged secret): no RCON/auth handling in game DLL, CLEAN. MOAD-0005 (thundering herd): single-threaded, no concurrent cache, CLEAN. --- .../regamedll-0002/patch/regamedll-0002.patch | 84 ++++++++ defects/regamedll-0002/test/test_node_exists | Bin 0 -> 18088 bytes .../regamedll-0002/test/test_node_exists.cpp | 198 ++++++++++++++++++ 3 files changed, 282 insertions(+) create mode 100644 defects/regamedll-0002/patch/regamedll-0002.patch create mode 100755 defects/regamedll-0002/test/test_node_exists create mode 100644 defects/regamedll-0002/test/test_node_exists.cpp 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 0000000000000000000000000000000000000000..ca9972f2234d8904b4aff770d64b56bb2498a1e3 GIT binary patch literal 18088 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 literal 0 HcmV?d00001 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; +}