Defect: projectile.cpp line 872, std::find on std::vector<BASE_OBJECT*> psDamaged inside grid neighbor iteration loop. Every projectile tick, for each nearby object, does O(D) linear scan to check if already damaged. Penetrating weapons inherit and grow psDamaged across hits. Fix: replace std::vector with std::unordered_set for O(1) lookup. push_back becomes insert, std::find becomes count, remove_if becomes iterator-based erase loop. Severity: MEDIUM. Hot path (per projectile per tick), scales with battle density. D=200 damaged, G=100 grid neighbors: 9.5x speedup. MOAD 0002-0005 CLEAN: - 0002: global state is architectural (Eidos-era C game), not coupling defect - 0003: no thread_local usage found - 0004: no secrets logged (public keys and IPs only, standard for server logs) - 0005: no unsynchronized cache patterns (game logic is single-threaded)
119 lines
4 KiB
C++
119 lines
4 KiB
C++
// Unit test for warzone2100-0001: PROJECTILE::psDamaged linear scan O(G*D)
|
|
// Defect: std::find on std::vector<BASE_OBJECT*> inside grid iteration loop
|
|
// Fix: std::unordered_set<BASE_OBJECT*> gives O(1) lookup instead of O(N)
|
|
//
|
|
// CWE-407: Algorithmic Complexity — list membership check in hot loop
|
|
|
|
#include <vector>
|
|
#include <unordered_set>
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <cstdio>
|
|
#include <cassert>
|
|
#include <cstdint>
|
|
|
|
// Simulate BASE_OBJECT as an opaque pointer (just need distinct addresses)
|
|
struct FakeObject {
|
|
uint32_t id;
|
|
};
|
|
|
|
// ---- BEFORE (vector + std::find) ----
|
|
static int projectile_collision_check_before(
|
|
const std::vector<FakeObject*>& psDamaged,
|
|
const std::vector<FakeObject*>& gridNeighbors)
|
|
{
|
|
int skipped = 0;
|
|
for (FakeObject* psTempObj : gridNeighbors)
|
|
{
|
|
if (std::find(psDamaged.begin(), psDamaged.end(), psTempObj) != psDamaged.end())
|
|
{
|
|
skipped++;
|
|
continue;
|
|
}
|
|
// ... collision detection would happen here
|
|
}
|
|
return skipped;
|
|
}
|
|
|
|
// ---- AFTER (unordered_set + count) ----
|
|
static int projectile_collision_check_after(
|
|
const std::unordered_set<FakeObject*>& psDamaged,
|
|
const std::vector<FakeObject*>& gridNeighbors)
|
|
{
|
|
int skipped = 0;
|
|
for (FakeObject* psTempObj : gridNeighbors)
|
|
{
|
|
if (psDamaged.count(psTempObj) != 0)
|
|
{
|
|
skipped++;
|
|
continue;
|
|
}
|
|
// ... collision detection would happen here
|
|
}
|
|
return skipped;
|
|
}
|
|
|
|
int main()
|
|
{
|
|
// Simulate a penetrating projectile that has passed through many objects.
|
|
// D = damaged count (objects already hit by this projectile)
|
|
// G = grid neighbor count (objects near projectile to check each tick)
|
|
const int D = 200; // penetrating area-effect weapon in a dense battle
|
|
const int G = 100; // grid neighbors in PROJ_NEIGHBOUR_RANGE
|
|
|
|
// Create fake objects
|
|
std::vector<FakeObject> allObjects(D + G);
|
|
for (int i = 0; i < D + G; i++) {
|
|
allObjects[i].id = i;
|
|
}
|
|
|
|
// Build psDamaged list (first D objects already damaged)
|
|
std::vector<FakeObject*> psDamagedVec;
|
|
std::unordered_set<FakeObject*> psDamagedSet;
|
|
for (int i = 0; i < D; i++) {
|
|
psDamagedVec.push_back(&allObjects[i]);
|
|
psDamagedSet.insert(&allObjects[i]);
|
|
}
|
|
|
|
// Build grid neighbors: half already damaged, half new
|
|
std::vector<FakeObject*> gridNeighbors;
|
|
for (int i = D/2; i < D/2 + G; i++) {
|
|
gridNeighbors.push_back(&allObjects[i]);
|
|
}
|
|
|
|
// Correctness check
|
|
int skipBefore = projectile_collision_check_before(psDamagedVec, gridNeighbors);
|
|
int skipAfter = projectile_collision_check_after(psDamagedSet, gridNeighbors);
|
|
assert(skipBefore == skipAfter);
|
|
printf("PASS correctness: both skip %d objects\n", skipBefore);
|
|
|
|
// Benchmark: simulate many projectile ticks
|
|
const int TICKS = 5000;
|
|
|
|
auto t0 = std::chrono::high_resolution_clock::now();
|
|
volatile int sinkBefore = 0;
|
|
for (int t = 0; t < TICKS; t++) {
|
|
sinkBefore += projectile_collision_check_before(psDamagedVec, gridNeighbors);
|
|
}
|
|
auto t1 = std::chrono::high_resolution_clock::now();
|
|
volatile int sinkAfter = 0;
|
|
for (int t = 0; t < TICKS; t++) {
|
|
sinkAfter += projectile_collision_check_after(psDamagedSet, gridNeighbors);
|
|
}
|
|
auto t2 = std::chrono::high_resolution_clock::now();
|
|
|
|
double msBefore = std::chrono::duration<double, std::milli>(t1 - t0).count();
|
|
double msAfter = std::chrono::duration<double, std::milli>(t2 - t1).count();
|
|
double ratio = msBefore / msAfter;
|
|
|
|
printf("BEFORE (vector std::find): %.2f ms (%d ticks)\n", msBefore, TICKS);
|
|
printf("AFTER (unordered_set): %.2f ms (%d ticks)\n", msAfter, TICKS);
|
|
printf("Speedup ratio: %.1fx\n", ratio);
|
|
|
|
// Patched version must be faster
|
|
assert(ratio > 2.0 && "Expected at least 2x speedup from set lookup");
|
|
printf("PASS performance: %.1fx speedup (D=%d, G=%d)\n", ratio, D, G);
|
|
|
|
printf("\nAll tests PASS\n");
|
|
return 0;
|
|
}
|