diff --git a/UNDF-REGISTRY.json b/UNDF-REGISTRY.json index 377bd7e48..b3cf60175 100644 --- a/UNDF-REGISTRY.json +++ b/UNDF-REGISTRY.json @@ -943,5 +943,6 @@ "monogame-0002-0002": "UNDF-2026-000000942", "monogame-0003-0003": "UNDF-2026-000000943", "supertuxkart-0001-0001": "UNDF-2026-000000944", - "supertuxkart-0002-0002": "UNDF-2026-000000945" + "supertuxkart-0002-0002": "UNDF-2026-000000945", + "minetest-0004-0004": "UNDF-2026-000000946" } diff --git a/defects/cataclysm-0001/patch/cataclysm-0001.patch b/defects/cataclysm-0001/patch/cataclysm-0001.patch new file mode 100644 index 000000000..704db2b5c --- /dev/null +++ b/defects/cataclysm-0001/patch/cataclysm-0001.patch @@ -0,0 +1,31 @@ +# UNDF: UNDF-2026-000000935 +--- a/src/overmap_ui.cpp ++++ b/src/overmap_ui.cpp +@@ -1,5 +1,6 @@ + // ... (includes) + #include ++#include + + // In overmap_ui::search() + // BEFORE (CWE-407): overmap_checked is std::vector with std::find for dedup +@@ -7,7 +8,7 @@ + // radius = OMAPX * 5 = 900, iterating ~3.24M points + // Each std::find is O(M) where M = overmaps already checked + // Total: O(P * M) where P = points in radius, M = distinct overmaps +- std::vector overmap_checked; ++ std::unordered_set overmap_checked; + + const int radius = OMAPX * 5; // arbitrary + for( const tripoint_abs_omt &p : points_in_radius( curs, radius ) ) { +@@ -16,9 +17,8 @@ + tripoint_om_omt om_relative = om_loc.local; + point_abs_om om_cache = project_to( p.xy() ); + +- if( std::find( overmap_checked.begin(), overmap_checked.end(), +- om_cache ) == overmap_checked.end() ) { +- overmap_checked.push_back( om_cache ); ++ if( overmap_checked.find( om_cache ) == overmap_checked.end() ) { ++ overmap_checked.insert( om_cache ); + std::vector notes = om_loc.om->find_notes( curs.z(), term ); + locations.insert( locations.end(), notes.begin(), notes.end() ); + } diff --git a/defects/cataclysm-0001/test/cataclysm-0001-test.cpp b/defects/cataclysm-0001/test/cataclysm-0001-test.cpp new file mode 100644 index 000000000..4f162212f --- /dev/null +++ b/defects/cataclysm-0001/test/cataclysm-0001-test.cpp @@ -0,0 +1,80 @@ +// Unit test for cataclysm-0001: overmap_ui search dedup +// Verifies that unordered_set dedup matches vector+std::find dedup +// and measures O(1) vs O(N) lookup performance. + +#include +#include +#include +#include +#include +#include + +// Simulate point_abs_om as a simple pair +struct point_abs_om { + int x, y; + bool operator==( const point_abs_om &o ) const { return x == o.x && y == o.y; } +}; + +struct point_abs_om_hash { + size_t operator()( const point_abs_om &p ) const { + return std::hash()( p.x ) ^ ( std::hash()( p.y ) << 16 ); + } +}; + +int main() { + // Generate overmap coordinates as if scanning radius 900 + // In practice, distinct overmaps = (2*900/180+1)^2 ~ 100 + const int num_overmaps = 100; + const int num_lookups = 50000; // simulating many points mapping to same overmaps + + std::vector points; + for( int i = 0; i < num_lookups; i++ ) { + points.push_back( { i % num_overmaps, i / num_overmaps } ); + } + + // BEFORE: vector + std::find + { + std::vector checked; + int found_count = 0; + auto t0 = std::chrono::high_resolution_clock::now(); + for( const auto &p : points ) { + if( std::find( checked.begin(), checked.end(), p ) == checked.end() ) { + checked.push_back( p ); + } else { + found_count++; + } + } + auto t1 = std::chrono::high_resolution_clock::now(); + double ms_before = std::chrono::duration( t1 - t0 ).count(); + printf( "BEFORE (vector+find): %.3f ms, %d unique, %d dupes\n", + ms_before, (int)checked.size(), found_count ); + + // AFTER: unordered_set + std::unordered_set checked_set; + int found_count2 = 0; + auto t2 = std::chrono::high_resolution_clock::now(); + for( const auto &p : points ) { + if( checked_set.find( p ) == checked_set.end() ) { + checked_set.insert( p ); + } else { + found_count2++; + } + } + auto t3 = std::chrono::high_resolution_clock::now(); + double ms_after = std::chrono::duration( t3 - t2 ).count(); + printf( "AFTER (unordered_set): %.3f ms, %zu unique, %d dupes\n", + ms_after, checked_set.size(), found_count2 ); + + // Verify correctness + assert( checked.size() == checked_set.size() ); + assert( found_count == found_count2 ); + + double ratio = ms_before / ms_after; + printf( "Speedup ratio: %.1fx\n", ratio ); + assert( ratio > 1.5 ); // Must be measurably faster + + printf( "PASS: cataclysm-0001 overmap search dedup\n" ); + } + + return 0; +} diff --git a/defects/cataclysm-0002/patch/cataclysm-0002.patch b/defects/cataclysm-0002/patch/cataclysm-0002.patch new file mode 100644 index 000000000..3b271772f --- /dev/null +++ b/defects/cataclysm-0002/patch/cataclysm-0002.patch @@ -0,0 +1,74 @@ +# UNDF: UNDF-2026-000000936 +--- a/src/dependency_tree.cpp ++++ b/src/dependency_tree.cpp +@@ -1,5 +1,6 @@ + #include "dependency_tree.h" + + #include ++#include + #include + #include + +@@ -103,14 +104,15 @@ + void dependency_node::inherit_errors() + { + std::stack nodes_to_check; + std::set nodes_visited; + + // ... (BFS traversal) ... + // BEFORE (CWE-407): cur_errors is a copy of all_errors[error_type], + // std::find scans it linearly for each node_error. O(E*N). ++ // Also note: cur_errors is a stale copy, dedup is partially broken. + // +- std::vector cur_errors = all_errors[error_type]; +- for( auto &node_error : node_errors ) { +- if( std::find( cur_errors.begin(), cur_errors.end(), node_error ) == +- cur_errors.end() ) { ++ std::unordered_set cur_errors_set( ++ all_errors[error_type].begin(), all_errors[error_type].end() ); ++ for( const auto &node_error : node_errors ) { ++ if( cur_errors_set.find( node_error ) == cur_errors_set.end() ) { + all_errors[cerror.first].push_back( node_error ); ++ cur_errors_set.insert( node_error ); + } + } + +@@ -157,6 +159,7 @@ + std::vector dependency_node::get_dependencies_as_nodes() + { + std::vector dependencies; + std::vector ret; ++ std::unordered_set ret_seen; + std::set found; + + // ... (BFS collection into dependencies) ... +@@ -188,8 +191,9 @@ + // BEFORE (CWE-407): std::find on ret vector for dedup, O(N^2) + for( std::vector::reverse_iterator it = + dependencies.rbegin(); + it != dependencies.rend(); ++it ) { +- if( std::find( ret.begin(), ret.end(), *it ) == ret.end() ) { ++ if( ret_seen.find( *it ) == ret_seen.end() ) { + ret.push_back( *it ); ++ ret_seen.insert( *it ); + } + } + +@@ -216,6 +220,7 @@ + std::vector dependency_node::get_dependents_as_nodes() + { + std::vector dependents; + std::vector ret; ++ std::unordered_set ret_seen; + std::set found; + + // ... (BFS collection into dependents) ... +@@ -244,8 +249,9 @@ + // BEFORE (CWE-407): std::find on ret vector for dedup, O(N^2) + for( dependency_node *&dependent : dependents ) { +- if( std::find( ret.begin(), ret.end(), dependent ) == ret.end() ) { ++ if( ret_seen.find( dependent ) == ret_seen.end() ) { + ret.push_back( dependent ); ++ ret_seen.insert( dependent ); + } + } diff --git a/defects/cataclysm-0002/test/cataclysm-0002-test.cpp b/defects/cataclysm-0002/test/cataclysm-0002-test.cpp new file mode 100644 index 000000000..f48c9758d --- /dev/null +++ b/defects/cataclysm-0002/test/cataclysm-0002-test.cpp @@ -0,0 +1,130 @@ +// Unit test for cataclysm-0002: dependency_tree dedup +// Verifies that unordered_set dedup matches vector+std::find dedup +// for get_dependencies_as_nodes and get_dependents_as_nodes patterns. + +#include +#include +#include +#include +#include +#include +#include + +// Simulate dependency_node pointer dedup +struct FakeNode { + int id; +}; + +int main() { + const int N = 500; // Number of dependency nodes + + // Create fake nodes + std::vector nodes( N ); + for( int i = 0; i < N; i++ ) { + nodes[i].id = i; + } + + // Simulate dependencies list with duplicates (as in BFS traversal) + std::vector dependencies; + for( int i = 0; i < N; i++ ) { + dependencies.push_back( &nodes[i] ); + // Add some duplicates + if( i % 3 == 0 ) { + dependencies.push_back( &nodes[i / 2] ); + } + } + + // BEFORE: vector + std::find for dedup + std::vector ret_before; + auto t0 = std::chrono::high_resolution_clock::now(); + for( int run = 0; run < 1000; run++ ) { + ret_before.clear(); + for( auto it = dependencies.rbegin(); it != dependencies.rend(); ++it ) { + if( std::find( ret_before.begin(), ret_before.end(), *it ) == ret_before.end() ) { + ret_before.push_back( *it ); + } + } + } + auto t1 = std::chrono::high_resolution_clock::now(); + double ms_before = std::chrono::duration( t1 - t0 ).count(); + + // AFTER: unordered_set + vector for dedup + std::vector ret_after; + auto t2 = std::chrono::high_resolution_clock::now(); + for( int run = 0; run < 1000; run++ ) { + ret_after.clear(); + std::unordered_set seen; + for( auto it = dependencies.rbegin(); it != dependencies.rend(); ++it ) { + if( seen.find( *it ) == seen.end() ) { + ret_after.push_back( *it ); + seen.insert( *it ); + } + } + } + auto t3 = std::chrono::high_resolution_clock::now(); + double ms_after = std::chrono::duration( t3 - t2 ).count(); + + printf( "BEFORE (vector+find): %.3f ms\n", ms_before ); + printf( "AFTER (unordered_set): %.3f ms\n", ms_after ); + + // Verify correctness: same elements in same order + assert( ret_before.size() == ret_after.size() ); + for( size_t i = 0; i < ret_before.size(); i++ ) { + assert( ret_before[i] == ret_after[i] ); + } + + double ratio = ms_before / ms_after; + printf( "Speedup ratio: %.1fx\n", ratio ); + assert( ratio > 1.5 ); + + // Also test inherit_errors pattern (string dedup) + { + const int E = 200; + std::vector errors; + for( int i = 0; i < E; i++ ) { + errors.push_back( "error_" + std::to_string( i ) ); + } + // Add duplicates + std::vector new_errors; + for( int i = 0; i < E; i++ ) { + new_errors.push_back( "error_" + std::to_string( i % ( E / 2 ) ) ); + } + + // BEFORE: vector copy + std::find + std::vector result_before = errors; + auto t4 = std::chrono::high_resolution_clock::now(); + for( int run = 0; run < 1000; run++ ) { + result_before = errors; + for( const auto &e : new_errors ) { + if( std::find( result_before.begin(), result_before.end(), e ) == result_before.end() ) { + result_before.push_back( e ); + } + } + } + auto t5 = std::chrono::high_resolution_clock::now(); + + // AFTER: unordered_set + vector + std::vector result_after; + auto t6 = std::chrono::high_resolution_clock::now(); + for( int run = 0; run < 1000; run++ ) { + result_after = errors; + std::unordered_set seen_set( errors.begin(), errors.end() ); + for( const auto &e : new_errors ) { + if( seen_set.find( e ) == seen_set.end() ) { + result_after.push_back( e ); + seen_set.insert( e ); + } + } + } + auto t7 = std::chrono::high_resolution_clock::now(); + + double ms_b = std::chrono::duration( t5 - t4 ).count(); + double ms_a = std::chrono::duration( t7 - t6 ).count(); + printf( "inherit_errors BEFORE: %.3f ms, AFTER: %.3f ms, ratio: %.1fx\n", + ms_b, ms_a, ms_b / ms_a ); + assert( result_before.size() == result_after.size() ); + } + + printf( "PASS: cataclysm-0002 dependency_tree dedup\n" ); + return 0; +} diff --git a/defects/cataclysm-0003/patch/cataclysm-0003.patch b/defects/cataclysm-0003/patch/cataclysm-0003.patch new file mode 100644 index 000000000..c8989abcf --- /dev/null +++ b/defects/cataclysm-0003/patch/cataclysm-0003.patch @@ -0,0 +1,45 @@ +# UNDF: UNDF-2026-000000937 +--- a/src/surroundings_menu.cpp ++++ b/src/surroundings_menu.cpp +@@ -1,5 +1,6 @@ + // ... (includes) + #include ++#include + + // In item_tab_data::add_item_recursive() + // BEFORE (CWE-407): item_order is std::vector with std::find +@@ -241,7 +242,8 @@ + void item_tab_data::add_item_recursive( std::vector &item_order, const item *it, + const tripoint_rel_ms &relative_pos ) + { ++ // Also pass item_order_set for O(1) membership check + const std::string name = it->tname(); + +- if( std::find( item_order.begin(), item_order.end(), name ) == item_order.end() ) { ++ if( item_order_set.find( name ) == item_order_set.end() ) { + item_order.push_back( name ); ++ item_order_set.insert( name ); + + items[name] = map_entity_stack( it, relative_pos, it->count() ); + } else { +@@ -259,6 +261,7 @@ + void item_tab_data::find_nearby_items( const Character &you, map &m ) + { + std::vector item_order; ++ // Add std::unordered_set item_order_set as class member + + // Similarly in terfurn_tab_data::add_terfurn() + // BEFORE: +@@ -585,7 +588,8 @@ + void terfurn_tab_data::add_terfurn( std::vector &item_order, + const map_data_common_t *terfurn, const tripoint_rel_ms &relative_pos ) + { + const std::string name = terfurn->name(); + +- if( std::find( item_order.begin(), item_order.end(), name ) == item_order.end() ) { ++ if( item_order_set.find( name ) == item_order_set.end() ) { + item_order.push_back( name ); ++ item_order_set.insert( name ); + + terfurns[name] = map_entity_stack( terfurn, relative_pos ); + } else { diff --git a/defects/cataclysm-0003/test/cataclysm-0003-test.cpp b/defects/cataclysm-0003/test/cataclysm-0003-test.cpp new file mode 100644 index 000000000..0bbe73ff0 --- /dev/null +++ b/defects/cataclysm-0003/test/cataclysm-0003-test.cpp @@ -0,0 +1,86 @@ +// Unit test for cataclysm-0003: surroundings_menu item/terfurn dedup +// Verifies that unordered_set dedup matches vector+std::find dedup +// for add_item_recursive and add_terfurn patterns. + +#include +#include +#include +#include +#include +#include +#include + +int main() { + // Simulate nearby items: radius 12 means ~(25)^2 = 625 tiles + // Each tile may have 0-5 items. With item stacking, many names repeat. + const int num_items = 2000; + const int unique_names = 300; + + std::vector item_names; + for( int i = 0; i < num_items; i++ ) { + item_names.push_back( "item_type_" + std::to_string( i % unique_names ) ); + } + + // BEFORE: vector + std::find for dedup + auto t0 = std::chrono::high_resolution_clock::now(); + for( int run = 0; run < 500; run++ ) { + std::vector item_order; + for( const auto &name : item_names ) { + if( std::find( item_order.begin(), item_order.end(), name ) == item_order.end() ) { + item_order.push_back( name ); + } + } + } + auto t1 = std::chrono::high_resolution_clock::now(); + double ms_before = std::chrono::duration( t1 - t0 ).count(); + + // Capture result for correctness check + std::vector result_before; + for( const auto &name : item_names ) { + if( std::find( result_before.begin(), result_before.end(), name ) == result_before.end() ) { + result_before.push_back( name ); + } + } + + // AFTER: unordered_set + vector for dedup + auto t2 = std::chrono::high_resolution_clock::now(); + for( int run = 0; run < 500; run++ ) { + std::vector item_order; + std::unordered_set item_order_set; + for( const auto &name : item_names ) { + if( item_order_set.find( name ) == item_order_set.end() ) { + item_order.push_back( name ); + item_order_set.insert( name ); + } + } + } + auto t3 = std::chrono::high_resolution_clock::now(); + double ms_after = std::chrono::duration( t3 - t2 ).count(); + + // Capture result for correctness check + std::vector result_after; + std::unordered_set result_set; + for( const auto &name : item_names ) { + if( result_set.find( name ) == result_set.end() ) { + result_after.push_back( name ); + result_set.insert( name ); + } + } + + printf( "BEFORE (vector+find): %.3f ms\n", ms_before ); + printf( "AFTER (unordered_set): %.3f ms\n", ms_after ); + + // Verify correctness: same elements in same order + assert( result_before.size() == result_after.size() ); + for( size_t i = 0; i < result_before.size(); i++ ) { + assert( result_before[i] == result_after[i] ); + } + assert( result_before.size() == static_cast( unique_names ) ); + + double ratio = ms_before / ms_after; + printf( "Speedup ratio: %.1fx\n", ratio ); + assert( ratio > 2.0 ); // Must show clear improvement + + printf( "PASS: cataclysm-0003 surroundings_menu dedup\n" ); + return 0; +}