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.
198 lines
5.8 KiB
C++
198 lines
5.8 KiB
C++
// 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 <cstdio>
|
|
#include <cstdint>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <unordered_set>
|
|
#include <cassert>
|
|
|
|
// 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<int64_t> g_nodeCoordSet;
|
|
static FakeNode g_pnodes[MAX_NODES];
|
|
static int g_pnodeCount = 0;
|
|
|
|
static int64_t PackCoord(int offsetX, int offsetY) {
|
|
return (static_cast<int64_t>(offsetX) << 32) | static_cast<uint32_t>(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;
|
|
}
|