Cataclysm-DDA/tests/visitable_remove_test.cpp

517 lines
22 KiB
C++

#include <algorithm>
#include <functional>
#include <list>
#include <memory>
#include <new>
#include <vector>
#include "calendar.h"
#include "cata_utility.h"
#include "cata_catch.h"
#include "character.h"
#include "inventory.h"
#include "item.h"
#include "itype.h"
#include "map.h"
#include "map_selector.h"
#include "optional.h"
#include "pimpl.h"
#include "player_helpers.h"
#include "point.h"
#include "rng.h"
#include "type_id.h"
#include "units.h"
#include "vehicle.h"
#include "vehicle_selector.h"
#include "visitable.h"
#include "vpart_position.h"
static const itype_id itype_bone( "bone" );
static const itype_id itype_bottle_plastic( "bottle_plastic" );
static const itype_id itype_flask_hip( "flask_hip" );
static const itype_id itype_water( "water" );
static const vproto_id vehicle_prototype_shopping_cart( "shopping_cart" );
template <typename T>
static int count_items( const T &src, const itype_id &id )
{
int n = 0;
src.visit_items( [&n, &id]( const item * e, item * ) {
n += ( e->typeId() == id );
return VisitResponse::NEXT;
} );
return n;
}
// NOLINTNEXTLINE(readability-function-size)
TEST_CASE( "visitable_remove", "[visitable]" )
{
const itype_id liquid_id = itype_water;
const itype_id container_id = itype_bottle_plastic;
const itype_id worn_id = itype_flask_hip;
const int count = 5;
REQUIRE( item( container_id ).is_container() );
REQUIRE( item( worn_id ).is_container() );
clear_avatar();
Character &p = get_player_character();
p.worn.wear_item( p, item( "backpack" ), false, false );
p.wear_item( item( "backpack" ) ); // so we don't drop anything
map &here = get_map();
// check if all tiles within radius are loaded within current submap and passable
const auto suitable = [&here]( const tripoint & pos, const int radius ) {
std::vector<tripoint> tiles = closest_points_first( pos, radius );
return std::all_of( tiles.begin(), tiles.end(), [&here]( const tripoint & e ) {
if( !here.inbounds( e ) ) {
return false;
}
if( const optional_vpart_position vp = here.veh_at( e ) ) {
here.destroy_vehicle( &vp->vehicle() );
}
here.i_clear( e );
return here.passable( e );
} );
};
// move player randomly until we find a suitable position
constexpr int num_trials = 100;
for( int i = 0; i < num_trials && !suitable( p.pos(), 1 ); ++i ) {
CHECK( !p.in_vehicle );
p.setpos( random_entry( closest_points_first( p.pos(), 1 ) ) );
}
REQUIRE( suitable( p.pos(), 1 ) );
item temp_liquid( liquid_id );
item obj = temp_liquid.in_container( temp_liquid.type->default_container.value_or( "null" ) );
REQUIRE( obj.num_item_stacks() == 1 );
const auto has_liquid_filter = [&liquid_id]( const item & it ) {
return it.typeId() == liquid_id;
};
REQUIRE( obj.has_item_with( has_liquid_filter ) );
GIVEN( "A player with several bottles of water" ) {
for( int i = 0; i != count; ++i ) {
p.i_add( obj );
}
REQUIRE( count_items( p, container_id ) == count );
REQUIRE( count_items( p, liquid_id ) == count );
WHEN( "all the bottles are removed" ) {
std::list<item> del = p.remove_items_with( [&container_id]( const item & e ) {
return e.typeId() == container_id;
} );
THEN( "no bottles remain in the players possession" ) {
REQUIRE( count_items( p, container_id ) == 0 );
}
THEN( "no water remain in the players possession" ) {
REQUIRE( count_items( p, liquid_id ) == 0 );
}
THEN( "the correct number of items were removed" ) {
REQUIRE( del.size() == count );
AND_THEN( "the removed items were all bottles" ) {
CHECK( std::all_of( del.begin(), del.end(), [&container_id]( const item & e ) {
return e.typeId() == container_id;
} ) );
}
AND_THEN( "the removed items all contain water" ) {
CHECK( std::all_of( del.begin(), del.end(), [&has_liquid_filter]( const item & e ) {
return e.num_item_stacks() == 1 && e.has_item_with( has_liquid_filter );
} ) );
}
}
}
WHEN( "one of the bottles is removed" ) {
std::list<item> del = p.remove_items_with( [&container_id]( const item & e ) {
return e.typeId() == container_id;
}, 1 );
THEN( "there is one less bottle in the players possession" ) {
REQUIRE( count_items( p, container_id ) == count - 1 );
}
THEN( "there is one less water in the players possession" ) {
REQUIRE( count_items( p, liquid_id ) == count - 1 );
}
THEN( "the correct number of items were removed" ) {
REQUIRE( del.size() == 1 );
AND_THEN( "the removed items were all bottles" ) {
CHECK( std::all_of( del.begin(), del.end(), [&container_id]( const item & e ) {
return e.typeId() == container_id;
} ) );
}
AND_THEN( "the removed items all contained water" ) {
CHECK( std::all_of( del.begin(), del.end(), [&has_liquid_filter]( const item & e ) {
return e.num_item_stacks() == 1 && e.has_item_with( has_liquid_filter );
} ) );
}
}
}
WHEN( "one of the bottles is wielded" ) {
p.wield( p.worn.front().legacy_front() );
REQUIRE( p.get_wielded_item()->typeId() == container_id );
REQUIRE( count_items( p, container_id ) == count );
REQUIRE( count_items( p, liquid_id ) == count );
AND_WHEN( "all the bottles are removed" ) {
std::list<item> del = p.remove_items_with( [&container_id]( const item & e ) {
return e.typeId() == container_id;
} );
THEN( "no bottles remain in the players possession" ) {
REQUIRE( count_items( p, container_id ) == 0 );
}
THEN( "no water remain in the players possession" ) {
REQUIRE( count_items( p, liquid_id ) == 0 );
}
THEN( "there is no currently wielded item" ) {
REQUIRE( !p.get_wielded_item() );
}
THEN( "the correct number of items were removed" ) {
REQUIRE( del.size() == count );
AND_THEN( "the removed items were all bottles" ) {
CHECK( std::all_of( del.begin(), del.end(), [&container_id]( const item & e ) {
return e.typeId() == container_id;
} ) );
}
AND_THEN( "the removed items all contain water" ) {
CHECK( std::all_of( del.begin(), del.end(), [&has_liquid_filter]( const item & e ) {
return e.num_item_stacks() == 1 && e.has_item_with( has_liquid_filter );
} ) );
}
}
}
AND_WHEN( "all but one of the bottles is removed" ) {
std::list<item> del = p.remove_items_with( [&container_id]( const item & e ) {
return e.typeId() == container_id;
}, count - 1 );
THEN( "there is only one bottle remaining in the players possession" ) {
REQUIRE( count_items( p, container_id ) == 1 );
AND_THEN( "the remaining bottle is currently wielded" ) {
REQUIRE( p.get_wielded_item()->typeId() == container_id );
AND_THEN( "the remaining water is contained by the currently wielded bottle" ) {
REQUIRE( p.get_wielded_item()->num_item_stacks() == 1 );
REQUIRE( p.get_wielded_item()->has_item_with( has_liquid_filter ) );
}
}
}
THEN( "there is only one water remaining in the players possession" ) {
REQUIRE( count_items( p, liquid_id ) == 1 );
}
THEN( "the correct number of items were removed" ) {
REQUIRE( del.size() == count - 1 );
AND_THEN( "the removed items were all bottles" ) {
CHECK( std::all_of( del.begin(), del.end(), [&container_id]( const item & e ) {
return e.typeId() == container_id;
} ) );
}
AND_THEN( "the removed items all contained water" ) {
CHECK( std::all_of( del.begin(), del.end(), [&has_liquid_filter]( const item & e ) {
return e.num_item_stacks() == 1 && e.has_item_with( has_liquid_filter );
} ) );
}
}
}
}
WHEN( "a hip flask containing water is wielded" ) {
item obj( worn_id );
item liquid( liquid_id, calendar::turn );
liquid.charges -= obj.fill_with( liquid, liquid.charges );
p.wield( obj );
REQUIRE( count_items( p, container_id ) == count );
REQUIRE( count_items( p, liquid_id ) == count + 1 );
AND_WHEN( "all but one of the water is removed" ) {
std::list<item> del = p.remove_items_with( [&liquid_id]( const item & e ) {
return e.typeId() == liquid_id;
}, count );
THEN( "all of the bottles remain in the players possession" ) {
REQUIRE( count_items( p, container_id ) == 5 );
AND_THEN( "all of the bottles are now empty" ) {
REQUIRE( p.visit_items( [&container_id]( const item * e, item * ) {
return ( e->typeId() != container_id || e->empty() ) ?
VisitResponse::NEXT : VisitResponse::ABORT;
} ) != VisitResponse::ABORT );
}
}
THEN( "the hip flask remains in the players possession" ) {
auto found = p.items_with( [&worn_id]( const item & e ) {
return e.typeId() == worn_id;
} );
REQUIRE( found.size() == 1 );
AND_THEN( "the hip flask is still worn" ) {
REQUIRE( p.is_wielding( *found[0] ) );
AND_THEN( "the hip flask contains water" ) {
REQUIRE( found[0]->num_item_stacks() == 1 );
REQUIRE( found[0]->has_item_with( has_liquid_filter ) );
}
}
}
THEN( "there is only one water remaining in the players possession" ) {
REQUIRE( count_items( p, liquid_id ) == 1 );
}
THEN( "the correct number of items were removed" ) {
REQUIRE( del.size() == count );
AND_THEN( "the removed items were all water" ) {
CHECK( std::all_of( del.begin(), del.end(), [&liquid_id]( const item & e ) {
return e.typeId() == liquid_id;
} ) );
}
}
AND_WHEN( "the final water is removed" ) {
std::list<item> del = p.remove_items_with( [&liquid_id]( const item & e ) {
return e.typeId() == liquid_id;
}, 1 );
THEN( "no water remain in the players possession" ) {
REQUIRE( count_items( p, liquid_id ) == 0 );
}
THEN( "the hip flask remains in the players possession" ) {
auto found = p.items_with( [&worn_id]( const item & e ) {
return e.typeId() == worn_id;
} );
REQUIRE( found.size() == 1 );
AND_THEN( "the hip flask is worn" ) {
REQUIRE( p.is_wielding( *found[0] ) );
AND_THEN( "the hip flask is empty" ) {
REQUIRE( found[0]->empty() );
}
}
}
}
}
}
}
GIVEN( "A player surrounded by several bottles of water" ) {
std::vector<tripoint> tiles = closest_points_first( p.pos(), 1 );
tiles.erase( tiles.begin() ); // player tile
int our = 0; // bottles placed on player tile
int adj = 0; // bottles placed on adjacent tiles
for( int i = 0; i != count; ++i ) {
if( i == 0 || tiles.empty() ) {
// always place at least one bottle on player tile
our++;
here.add_item( p.pos(), obj );
} else {
// randomly place bottles on adjacent tiles
adj++;
here.add_item( random_entry( tiles ), obj );
}
}
REQUIRE( our + adj == count );
map_selector sel( p.pos(), 1 );
map_cursor cur( p.pos() );
REQUIRE( count_items( sel, container_id ) == count );
REQUIRE( count_items( sel, liquid_id ) == count );
REQUIRE( count_items( cur, container_id ) == our );
REQUIRE( count_items( cur, liquid_id ) == our );
WHEN( "all the bottles are removed" ) {
std::list<item> del = sel.remove_items_with( [&container_id]( const item & e ) {
return e.typeId() == container_id;
} );
THEN( "no bottles remain on the map" ) {
REQUIRE( count_items( sel, container_id ) == 0 );
}
THEN( "no water remains on the map" ) {
REQUIRE( count_items( sel, liquid_id ) == 0 );
}
THEN( "the correct number of items were removed" ) {
REQUIRE( del.size() == count );
AND_THEN( "the removed items were all bottles" ) {
CHECK( std::all_of( del.begin(), del.end(), [&container_id]( const item & e ) {
return e.typeId() == container_id;
} ) );
}
AND_THEN( "the removed items all contain water" ) {
CHECK( std::all_of( del.begin(), del.end(), [&has_liquid_filter]( const item & e ) {
return e.num_item_stacks() == 1 && e.has_item_with( has_liquid_filter );
} ) );
}
}
}
WHEN( "one of the bottles is removed" ) {
std::list<item> del = sel.remove_items_with( [&container_id]( const item & e ) {
return e.typeId() == container_id;
}, 1 );
THEN( "there is one less bottle on the map" ) {
REQUIRE( count_items( sel, container_id ) == count - 1 );
}
THEN( "there is one less water on the map" ) {
REQUIRE( count_items( sel, liquid_id ) == count - 1 );
}
THEN( "the correct number of items were removed" ) {
REQUIRE( del.size() == 1 );
AND_THEN( "the removed items were all bottles" ) {
CHECK( std::all_of( del.begin(), del.end(), [&container_id]( const item & e ) {
return e.typeId() == container_id;
} ) );
}
AND_THEN( "the removed items all contained water" ) {
CHECK( std::all_of( del.begin(), del.end(), [&has_liquid_filter]( const item & e ) {
return e.num_item_stacks() == 1 && e.has_item_with( has_liquid_filter );
} ) );
}
}
}
WHEN( "all of the bottles on the player tile are removed" ) {
std::list<item> del = cur.remove_items_with( [&container_id]( const item & e ) {
return e.typeId() == container_id;
}, our );
THEN( "no bottles remain on the player tile" ) {
REQUIRE( count_items( cur, container_id ) == 0 );
}
THEN( "no water remains on the player tile" ) {
REQUIRE( count_items( cur, liquid_id ) == 0 );
}
THEN( "the correct amount of bottles remains on the map" ) {
REQUIRE( count_items( sel, container_id ) == count - our );
}
THEN( "there correct amount of water remains on the map" ) {
REQUIRE( count_items( sel, liquid_id ) == count - our );
}
THEN( "the correct number of items were removed" ) {
REQUIRE( static_cast<int>( del.size() ) == our );
AND_THEN( "the removed items were all bottles" ) {
CHECK( std::all_of( del.begin(), del.end(), [&container_id]( const item & e ) {
return e.typeId() == container_id;
} ) );
}
AND_THEN( "the removed items all contained water" ) {
CHECK( std::all_of( del.begin(), del.end(), [has_liquid_filter]( const item & e ) {
return e.num_item_stacks() == 1 && e.has_item_with( has_liquid_filter );
} ) );
}
}
}
}
GIVEN( "An adjacent vehicle contains several bottles of water" ) {
std::vector<tripoint> tiles = closest_points_first( p.pos(), 1 );
tiles.erase( tiles.begin() ); // player tile
tripoint veh = random_entry( tiles );
REQUIRE( here.add_vehicle( vehicle_prototype_shopping_cart, veh, 0_degrees, 0, 0 ) );
REQUIRE( std::count_if( tiles.begin(), tiles.end(), [&here]( const tripoint & e ) {
return static_cast<bool>( here.veh_at( e ) );
} ) == 1 );
const cata::optional<vpart_reference> vp = here.veh_at( veh ).part_with_feature( "CARGO", true );
REQUIRE( vp );
vehicle *const v = &vp->vehicle();
const int part = vp->part_index();
REQUIRE( part >= 0 );
// Empty the vehicle of any cargo.
v->get_items( part ).clear();
for( int i = 0; i != count; ++i ) {
v->add_item( part, obj );
}
vehicle_selector sel( p.pos(), 1 );
REQUIRE( count_items( sel, container_id ) == count );
REQUIRE( count_items( sel, liquid_id ) == count );
WHEN( "all the bottles are removed" ) {
std::list<item> del = sel.remove_items_with( [&container_id]( const item & e ) {
return e.typeId() == container_id;
} );
THEN( "no bottles remain within the vehicle" ) {
REQUIRE( count_items( sel, container_id ) == 0 );
}
THEN( "no water remains within the vehicle" ) {
REQUIRE( count_items( sel, liquid_id ) == 0 );
}
THEN( "the correct number of items were removed" ) {
REQUIRE( del.size() == count );
AND_THEN( "the removed items were all bottles" ) {
CHECK( std::all_of( del.begin(), del.end(), [&container_id]( const item & e ) {
return e.typeId() == container_id;
} ) );
}
AND_THEN( "the removed items all contain water" ) {
CHECK( std::all_of( del.begin(), del.end(), [&has_liquid_filter]( const item & e ) {
return e.num_item_stacks() == 1 && e.has_item_with( has_liquid_filter );
} ) );
}
}
}
WHEN( "one of the bottles is removed" ) {
std::list<item> del = sel.remove_items_with( [&container_id]( const item & e ) {
return e.typeId() == container_id;
}, 1 );
THEN( "there is one less bottle within the vehicle" ) {
REQUIRE( count_items( sel, container_id ) == count - 1 );
}
THEN( "there is one less water within the vehicle" ) {
REQUIRE( count_items( sel, liquid_id ) == count - 1 );
}
THEN( "the correct number of items were removed" ) {
REQUIRE( del.size() == 1 );
AND_THEN( "the removed items were all bottles" ) {
CHECK( std::all_of( del.begin(), del.end(), [&container_id]( const item & e ) {
return e.typeId() == container_id;
} ) );
}
AND_THEN( "the removed items all contained water" ) {
CHECK( std::all_of( del.begin(), del.end(), [&has_liquid_filter]( const item & e ) {
return e.num_item_stacks() == 1 && e.has_item_with( has_liquid_filter );
} ) );
}
}
}
}
}
TEST_CASE( "inventory_remove_invalidates_binning_cache", "[visitable][inventory]" )
{
inventory inv;
std::list<item> items = { item( "bone" ) };
inv += items;
CHECK( inv.charges_of( itype_bone ) == 1 );
inv.remove_items_with( return_true<item> );
CHECK( inv.size() == 0 );
// The following used to be a heap use-after-free due to a caching bug.
// Now should be safe.
CHECK( inv.charges_of( itype_bone ) == 0 );
}