1 #include "pickup.h"
2 
3 #include <algorithm>
4 #include <cstddef>
5 #include <functional>
6 #include <iosfwd>
7 #include <list>
8 #include <map>
9 #include <memory>
10 #include <new>
11 #include <string>
12 #include <type_traits>
13 #include <utility>
14 #include <vector>
15 
16 #include "activity_actor_definitions.h"
17 #include "auto_pickup.h"
18 #include "cata_utility.h"
19 #include "catacharset.h"
20 #include "character.h"
21 #include "colony.h"
22 #include "color.h"
23 #include "cursesdef.h"
24 #include "debug.h"
25 #include "enums.h"
26 #include "game.h"
27 #include "input.h"
28 #include "item.h"
29 #include "item_location.h"
30 #include "item_search.h"
31 #include "item_stack.h"
32 #include "line.h"
33 #include "map.h"
34 #include "map_selector.h"
35 #include "mapdata.h"
36 #include "messages.h"
37 #include "optional.h"
38 #include "options.h"
39 #include "output.h"
40 #include "panels.h"
41 #include "player_activity.h"
42 #include "point.h"
43 #include "popup.h"
44 #include "ret_val.h"
45 #include "sdltiles.h"
46 #include "string_formatter.h"
47 #include "string_input_popup.h"
48 #include "translations.h"
49 #include "type_id.h"
50 #include "ui.h"
51 #include "ui_manager.h"
52 #include "units.h"
53 #include "units_utility.h"
54 #include "vehicle.h"
55 #include "vehicle_selector.h"
56 #include "vpart_position.h"
57 
58 using ItemCount = std::pair<item, int>;
59 using PickupMap = std::map<std::string, ItemCount>;
60 
61 static const itype_id itype_water( "water" );
62 
63 // Pickup helper functions
64 static bool pick_one_up( item_location &loc, int quantity, bool &got_water, bool &offered_swap,
65                          PickupMap &mapPickup, bool autopickup );
66 
67 static void show_pickup_message( const PickupMap &mapPickup );
68 
69 struct pickup_count {
70     bool pick = false;
71     //count is 0 if the whole stack is being picked up, nonzero otherwise.
72     int count = 0;
73 };
74 
select_autopickup_items(std::vector<std::list<item_stack::iterator>> & here,std::vector<pickup_count> & getitem)75 static bool select_autopickup_items( std::vector<std::list<item_stack::iterator>> &here,
76                                      std::vector<pickup_count> &getitem )
77 {
78     bool bFoundSomething = false;
79 
80     //Loop through Items lowest Volume first
81     bool bPickup = false;
82 
83     for( size_t iVol = 0, iNumChecked = 0; iNumChecked < here.size(); iVol++ ) {
84         for( size_t i = 0; i < here.size(); i++ ) {
85             bPickup = false;
86             item_stack::iterator begin_iterator = here[i].front();
87             if( begin_iterator->volume() / units::legacy_volume_factor == static_cast<int>( iVol ) ) {
88                 iNumChecked++;
89                 const std::string sItemName = begin_iterator->tname( 1, false );
90 
91                 //Check the Pickup Rules
92                 if( get_auto_pickup().check_item( sItemName ) == rule_state::WHITELISTED ) {
93                     bPickup = true;
94                 } else if( get_auto_pickup().check_item( sItemName ) != rule_state::BLACKLISTED ) {
95                     //No prematched pickup rule found
96                     //check rules in more detail
97                     get_auto_pickup().create_rule( &*begin_iterator );
98 
99                     if( get_auto_pickup().check_item( sItemName ) == rule_state::WHITELISTED ) {
100                         bPickup = true;
101                     }
102                 }
103 
104                 //Auto Pickup all items with Volume <= AUTO_PICKUP_VOL_LIMIT * 50 and Weight <= AUTO_PICKUP_ZERO * 50
105                 //items will either be in the autopickup list ("true") or unmatched ("")
106                 if( !bPickup ) {
107                     int weight_limit = get_option<int>( "AUTO_PICKUP_WEIGHT_LIMIT" );
108                     int volume_limit = get_option<int>( "AUTO_PICKUP_VOL_LIMIT" );
109                     if( weight_limit && volume_limit ) {
110                         if( begin_iterator->volume() <= units::from_milliliter( volume_limit * 50 ) &&
111                             begin_iterator->weight() <= weight_limit * 50_gram &&
112                             get_auto_pickup().check_item( sItemName ) != rule_state::BLACKLISTED ) {
113                             bPickup = true;
114                         }
115                     }
116                 }
117             }
118 
119             if( bPickup ) {
120                 getitem[i].pick = true;
121                 bFoundSomething = true;
122             }
123         }
124     }
125     return bFoundSomething;
126 }
127 
128 enum pickup_answer : int {
129     CANCEL = -1,
130     WIELD,
131     WEAR,
132     SPILL,
133     STASH,
134     NUM_ANSWERS
135 };
136 
handle_problematic_pickup(const item & it,bool & offered_swap,const std::string & explain)137 static pickup_answer handle_problematic_pickup( const item &it, bool &offered_swap,
138         const std::string &explain )
139 {
140     if( offered_swap ) {
141         return CANCEL;
142     }
143 
144     Character &u = get_player_character();
145 
146     uilist amenu;
147 
148     amenu.text = explain;
149 
150     offered_swap = true;
151     // TODO: Gray out if not enough hands
152     if( u.has_wield_conflicts( it ) ) {
153         amenu.addentry( WIELD, u.can_unwield( u.weapon ).success(), 'w',
154                         _( "Dispose of %s and wield %s" ), u.weapon.display_name(),
155                         it.display_name() );
156     } else {
157         amenu.addentry( WIELD, true, 'w', _( "Wield %s" ), it.display_name() );
158     }
159     if( it.is_armor() ) {
160         amenu.addentry( WEAR, u.can_wear( it ).success(), 'W', _( "Wear %s" ), it.display_name() );
161     }
162     if( it.is_bucket_nonempty() ) {
163         amenu.addentry( SPILL, u.can_stash( it ), 's', _( "Spill contents of %s, then pick up %s" ),
164                         it.tname(), it.display_name() );
165     }
166 
167     amenu.query();
168     int choice = amenu.ret;
169 
170     if( choice <= CANCEL || choice >= NUM_ANSWERS ) {
171         return CANCEL;
172     }
173 
174     return static_cast<pickup_answer>( choice );
175 }
176 
query_thief()177 bool Pickup::query_thief()
178 {
179     Character &u = get_player_character();
180     const bool force_uc = get_option<bool>( "FORCE_CAPITAL_YN" );
181     const auto &allow_key = force_uc ? input_context::disallow_lower_case_or_non_modified_letters
182                             : input_context::allow_all_keys;
183     std::string answer = query_popup()
184                          .preferred_keyboard_mode( keyboard_mode::keycode )
185                          .allow_cancel( false )
186                          .context( "YES_NO_ALWAYS_NEVER" )
187                          .message( "%s", force_uc && !is_keycode_mode_supported()
188                                    ? _( "Picking up this item will be considered stealing, continue?  (Case sensitive)" )
189                                    : _( "Picking up this item will be considered stealing, continue?" ) )
190                          .option( "YES", allow_key ) // yes, steal all items in this location that is selected
191                          .option( "NO", allow_key ) // no, pick up only what is free
192                          .option( "ALWAYS", allow_key ) // Yes, steal all items and stop asking me this question
193                          .option( "NEVER", allow_key ) // no, only grab free item and never ask me again
194                          .cursor( 1 ) // default to the second option `NO`
195                          .query()
196                          .action; // retrieve the input action
197     if( answer == "YES" ) {
198         u.set_value( "THIEF_MODE", "THIEF_STEAL" );
199         u.set_value( "THIEF_MODE_KEEP", "NO" );
200         return true;
201     } else if( answer == "NO" ) {
202         u.set_value( "THIEF_MODE", "THIEF_HONEST" );
203         u.set_value( "THIEF_MODE_KEEP", "NO" );
204         return false;
205     } else if( answer == "ALWAYS" ) {
206         u.set_value( "THIEF_MODE", "THIEF_STEAL" );
207         u.set_value( "THIEF_MODE_KEEP", "YES" );
208         return true;
209     } else if( answer == "NEVER" ) {
210         u.set_value( "THIEF_MODE", "THIEF_HONEST" );
211         u.set_value( "THIEF_MODE_KEEP", "YES" );
212         return false;
213     } else {
214         // error
215         debugmsg( "Not a valid option [ %s ]", answer );
216     }
217     return false;
218 }
219 
220 // Returns false if pickup caused a prompt and the player selected to cancel pickup
pick_one_up(item_location & loc,int quantity,bool & got_water,bool & offered_swap,PickupMap & mapPickup,bool autopickup)221 bool pick_one_up( item_location &loc, int quantity, bool &got_water, bool &offered_swap,
222                   PickupMap &mapPickup, bool autopickup )
223 {
224     Character &player_character = get_player_character();
225     int moves_taken = loc.obtain_cost( player_character, quantity );
226     bool picked_up = false;
227     pickup_answer option = CANCEL;
228 
229     // We already checked in do_pickup if this was a nullptr
230     // Make copies so the original remains untouched if we bail out
231     item_location newloc = loc;
232     //original item reference
233     item &it = *newloc.get_item();
234     //new item (copy)
235     item newit = it;
236 
237     if( !newit.is_owned_by( player_character, true ) ) {
238         // Has the player given input on if stealing is ok?
239         if( player_character.get_value( "THIEF_MODE" ) == "THIEF_ASK" ) {
240             Pickup::query_thief();
241         }
242         if( player_character.get_value( "THIEF_MODE" ) == "THIEF_HONEST" ) {
243             return true; // Since we are honest, return no problem before picking up
244         }
245     }
246     if( newit.invlet != '\0' &&
247         player_character.invlet_to_item( newit.invlet ) != nullptr ) {
248         // Existing invlet is not re-usable, remove it and let the code in player.cpp/inventory.cpp
249         // add a new invlet, otherwise keep the (usable) invlet.
250         newit.invlet = '\0';
251     }
252 
253     // Handle charges, quantity == 0 means move all
254     if( quantity != 0 && newit.count_by_charges() ) {
255         if( newit.charges > quantity ) {
256             newit.charges = quantity;
257         }
258     }
259 
260     bool did_prompt = false;
261     if( newit.is_frozen_liquid() ) {
262         if( !( got_water = !( player_character.crush_frozen_liquid( newloc ) ) ) ) {
263             option = STASH;
264         }
265     } else if( newit.made_of_from_type( phase_id::LIQUID ) && !newit.is_frozen_liquid() ) {
266         got_water = true;
267     } else if( !player_character.can_pickWeight_partial( newit, false ) ) {
268         if( !autopickup ) {
269             const std::string &explain = string_format( _( "The %s is too heavy!" ),
270                                          newit.display_name() );
271             option = handle_problematic_pickup( newit, offered_swap, explain );
272             did_prompt = true;
273         } else {
274             option = CANCEL;
275         }
276     } else if( newit.is_bucket_nonempty() ) {
277         if( !autopickup ) {
278             const std::string &explain = string_format( _( "Can't stash %s while it's not empty" ),
279                                          newit.display_name() );
280             option = handle_problematic_pickup( newit, offered_swap, explain );
281             did_prompt = true;
282         } else {
283             option = CANCEL;
284         }
285     } else if( !player_character.can_stash_partial( newit ) ) {
286         if( !autopickup ) {
287             const std::string &explain = string_format( _( "Not enough capacity to stash %s" ),
288                                          newit.display_name() );
289             option = handle_problematic_pickup( newit, offered_swap, explain );
290             did_prompt = true;
291         } else {
292             option = CANCEL;
293         }
294     } else {
295         option = STASH;
296     }
297 
298     switch( option ) {
299         case NUM_ANSWERS:
300             // Some other option
301             break;
302         case CANCEL:
303             picked_up = false;
304             break;
305         case WEAR:
306             picked_up = !!player_character.wear_item( newit );
307             break;
308         case WIELD: {
309             const auto wield_check = player_character.can_wield( newit );
310             if( wield_check.success() ) {
311                 picked_up = player_character.wield( newit );
312                 if( player_character.weapon.invlet ) {
313                     add_msg( m_info, _( "Wielding %c - %s" ), player_character.weapon.invlet,
314                              player_character.weapon.display_name() );
315                 } else {
316                     add_msg( m_info, _( "Wielding - %s" ), player_character.weapon.display_name() );
317                 }
318             } else {
319                 add_msg( m_neutral, wield_check.c_str() );
320             }
321             break;
322         }
323         case SPILL:
324             if( newit.is_container_empty() ) {
325                 debugmsg( "Tried to spill contents from an empty container" );
326                 break;
327             }
328             //using original item, possibly modifying it
329             picked_up = it.spill_contents( player_character );
330             if( !picked_up ) {
331                 break;
332             } else {
333                 const int invlet = newit.invlet;
334                 newit = it;
335                 newit.invlet = invlet;
336             }
337         // Intentional fallthrough
338         case STASH: {
339             item &added_it = player_character.i_add( newit, true, nullptr, /*allow_drop=*/false,
340                              !newit.count_by_charges() );
341             if( added_it.is_null() ) {
342                 // failed to add, fill pockets if it's a stack
343                 if( newit.count_by_charges() ) {
344                     int remaining_charges = newit.charges;
345                     if( player_character.weapon.can_contain_partial( newit ) ) {
346                         int used_charges = player_character.weapon.fill_with( newit, remaining_charges );
347                         remaining_charges -= used_charges;
348                     }
349                     for( item &i : player_character.worn ) {
350                         if( remaining_charges == 0 ) {
351                             break;
352                         }
353                         if( i.can_contain_partial( newit ) ) {
354                             int used_charges = i.fill_with( newit, remaining_charges );
355                             remaining_charges -= used_charges;
356                         }
357                     }
358                     newit.charges -= remaining_charges;
359                     newit.on_pickup( player_character );
360                     if( newit.charges != 0 ) {
361                         auto &entry = mapPickup[newit.tname()];
362                         entry.second += newit.charges;
363                         entry.first = newit;
364                         picked_up = true;
365                     }
366                 }
367             } else if( &added_it == &it ) {
368                 // merged to the original stack, restore original charges
369                 it.charges -= newit.charges;
370             } else {
371                 // successfully added
372                 auto &entry = mapPickup[newit.tname()];
373                 entry.second += newit.count();
374                 entry.first = newit;
375                 picked_up = true;
376             }
377 
378             break;
379         }
380     }
381 
382     if( picked_up ) {
383         item &orig_it = *loc.get_item();
384         // Subtract moved charges instead of assigning leftover charges,
385         // since the total charges of the original item may have changed
386         // due to merging.
387         if( orig_it.charges > newit.charges ) {
388             orig_it.charges -= newit.charges;
389         } else {
390             loc.remove_item();
391         }
392         player_character.moves -= moves_taken;
393         player_character.flag_encumbrance();
394         player_character.invalidate_weight_carried_cache();
395     }
396 
397     return picked_up || !did_prompt;
398 }
399 
do_pickup(std::vector<item_location> & targets,std::vector<int> & quantities,bool autopickup)400 bool Pickup::do_pickup( std::vector<item_location> &targets, std::vector<int> &quantities,
401                         bool autopickup )
402 {
403     bool got_water = false;
404     Character &player_character = get_player_character();
405     bool weight_is_okay = ( player_character.weight_carried() <= player_character.weight_capacity() );
406     bool offered_swap = false;
407 
408     // Map of items picked up so we can output them all at the end and
409     // merge dropping items with the same name.
410     PickupMap mapPickup;
411 
412     bool problem = false;
413     while( !problem && player_character.moves >= 0 && !targets.empty() ) {
414         item_location target = std::move( targets.back() );
415         int quantity = quantities.back();
416         // Whether we pick the item up or not, we're done trying to do so,
417         // so remove it from the list.
418         targets.pop_back();
419         quantities.pop_back();
420 
421         if( !target ) {
422             debugmsg( "lost target item of ACT_PICKUP" );
423             continue;
424         }
425 
426         problem = !pick_one_up( target, quantity, got_water, offered_swap, mapPickup, autopickup );
427     }
428 
429     if( !mapPickup.empty() ) {
430         show_pickup_message( mapPickup );
431     }
432 
433     if( got_water ) {
434         add_msg( m_info, _( "Spilt liquid cannot be picked back up.  Try mopping it instead." ) );
435     }
436     if( weight_is_okay && player_character.weight_carried() > player_character.weight_capacity() ) {
437         add_msg( m_bad, _( "You're overburdened!" ) );
438     }
439 
440     return !problem;
441 }
442 
443 // Pick up items at (pos).
pick_up(const tripoint & p,int min,from_where get_items_from)444 void Pickup::pick_up( const tripoint &p, int min, from_where get_items_from )
445 {
446     int cargo_part = -1;
447 
448     map &local = get_map();
449     const optional_vpart_position vp = local.veh_at( p );
450     vehicle *const veh = veh_pointer_or_null( vp );
451     bool from_vehicle = false;
452 
453     if( min != -1 ) {
454         if( veh != nullptr && get_items_from == prompt ) {
455             const cata::optional<vpart_reference> carg = vp.part_with_feature( "CARGO", false );
456             const bool veh_has_items = carg && !veh->get_items( carg->part_index() ).empty();
457             const bool map_has_items = local.has_items( p );
458             if( veh_has_items && map_has_items ) {
459                 uilist amenu( _( "Get items from where?" ), { _( "Get items from vehicle cargo" ), _( "Get items on the ground" ) } );
460                 if( amenu.ret == UILIST_CANCEL ) {
461                     return;
462                 }
463                 get_items_from = static_cast<from_where>( amenu.ret );
464             } else if( veh_has_items ) {
465                 get_items_from = from_cargo;
466             }
467         }
468         if( get_items_from == from_cargo ) {
469             const cata::optional<vpart_reference> carg = vp.part_with_feature( "CARGO", false );
470             cargo_part = carg ? carg->part_index() : -1;
471             from_vehicle = cargo_part >= 0;
472         } else {
473             // Nothing to change, default is to pick from ground anyway.
474             if( local.has_flag( "SEALED", p ) ) {
475                 return;
476             }
477         }
478     }
479 
480     if( !from_vehicle ) {
481         bool isEmpty = ( local.i_at( p ).empty() );
482 
483         // Hide the pickup window if this is a toilet and there's nothing here
484         // but non-frozen water.
485         if( ( !isEmpty ) && local.furn( p ) == f_toilet ) {
486             isEmpty = true;
487             for( const item &maybe_water : local.i_at( p ) ) {
488                 if( maybe_water.typeId() != itype_water  || maybe_water.is_frozen_liquid() ) {
489                     isEmpty = false;
490                     break;
491                 }
492             }
493         }
494 
495         if( isEmpty && ( min != -1 || !get_option<bool>( "AUTO_PICKUP_ADJACENT" ) ) ) {
496             return;
497         }
498     }
499 
500     // which items are we grabbing?
501     std::vector<item_stack::iterator> here;
502     if( from_vehicle ) {
503         vehicle_stack vehitems = veh->get_items( cargo_part );
504         for( item_stack::iterator it = vehitems.begin(); it != vehitems.end(); ++it ) {
505             here.push_back( it );
506         }
507     } else {
508         map_stack mapitems = local.i_at( p );
509         for( item_stack::iterator it = mapitems.begin(); it != mapitems.end(); ++it ) {
510             here.push_back( it );
511         }
512     }
513 
514     Character &player_character = get_player_character();
515     if( min == -1 ) {
516         // Recursively pick up adjacent items if that option is on.
517         if( get_option<bool>( "AUTO_PICKUP_ADJACENT" ) && player_character.pos() == p ) {
518             //Autopickup adjacent
519             direction adjacentDir[8] = {direction::NORTH, direction::NORTHEAST, direction::EAST, direction::SOUTHEAST, direction::SOUTH, direction::SOUTHWEST, direction::WEST, direction::NORTHWEST};
520             for( auto &elem : adjacentDir ) {
521 
522                 tripoint apos = tripoint( direction_XY( elem ), 0 );
523                 apos += p;
524 
525                 pick_up( apos, min );
526             }
527         }
528 
529         // Bail out if this square cannot be auto-picked-up
530         if( g->check_zone( zone_type_id( "NO_AUTO_PICKUP" ), p ) ) {
531             return;
532         } else if( local.has_flag( "SEALED", p ) ) {
533             return;
534         }
535     }
536 
537     // Not many items, just grab them
538     if( static_cast<int>( here.size() ) <= min && min != -1 ) {
539         if( from_vehicle ) {
540             player_character.assign_activity( player_activity( pickup_activity_actor(
541             { item_location( vehicle_cursor( *veh, cargo_part ), &*here.front() ) },
542             { 0 },
543             cata::nullopt
544                                               ) ) );
545         } else {
546             player_character.assign_activity( player_activity( pickup_activity_actor(
547             {item_location( map_cursor( p ), &*here.front() ) },
548             { 0 },
549             player_character.pos()
550                                               ) ) );
551         }
552         return;
553     }
554 
555     std::vector<std::list<item_stack::iterator>> stacked_here;
556     for( const item_stack::iterator &it : here ) {
557         bool found_stack = false;
558         for( std::list<item_stack::iterator> &stack : stacked_here ) {
559             if( stack.front()->display_stacked_with( *it ) ) {
560                 stack.push_back( it );
561                 found_stack = true;
562                 break;
563             }
564         }
565         if( !found_stack ) {
566             stacked_here.emplace_back( std::list<item_stack::iterator>( { it } ) );
567         }
568     }
569 
570     // Items are stored unordered in colonies on the map, so sort them for a nice display.
571     std::sort( stacked_here.begin(), stacked_here.end(), []( const auto & lhs, const auto & rhs ) {
572         return *lhs.front() < *rhs.front();
573     } );
574 
575     std::vector<pickup_count> getitem( stacked_here.size() );
576 
577     if( min == -1 ) { //Auto Pickup, select matching items
578         if( !select_autopickup_items( stacked_here, getitem ) ) {
579             // If we didn't find anything, bail out now.
580             return;
581         }
582     } else {
583         g->temp_exit_fullscreen();
584 
585         int start = 0;
586         int selected = 0;
587         int maxitems = 0;
588         int pickupH = 0;
589         int pickupW = 44;
590         int pickupX = 0;
591         catacurses::window w_pickup;
592         catacurses::window w_item_info;
593 
594         ui_adaptor ui;
595         ui.on_screen_resize( [&]( ui_adaptor & ui ) {
596             const int itemsH = std::min( 25, TERMY / 2 );
597             const int pickupBorderRows = 4;
598 
599             // The pickup list may consume the entire terminal, minus space needed for its
600             // header/footer and the item info window.
601             const int minleftover = itemsH + pickupBorderRows;
602             const int maxmaxitems = TERMY - minleftover;
603             const int minmaxitems = 9;
604             maxitems = clamp<int>( stacked_here.size(), minmaxitems, maxmaxitems );
605 
606             start = selected - selected % maxitems;
607 
608             pickupH = maxitems + pickupBorderRows;
609 
610             //find max length of item name and resize pickup window width
611             for( const std::list<item_stack::iterator> &cur_list : stacked_here ) {
612                 const item &this_item = *cur_list.front();
613                 const int item_len = utf8_width( remove_color_tags( this_item.display_name() ) ) + 10;
614                 if( item_len > pickupW && item_len < TERMX ) {
615                     pickupW = item_len;
616                 }
617             }
618 
619             pickupX = 0;
620             std::string position = get_option<std::string>( "PICKUP_POSITION" );
621             if( position == "left" ) {
622                 pickupX = panel_manager::get_manager().get_width_left();
623             } else if( position == "right" ) {
624                 pickupX = TERMX - panel_manager::get_manager().get_width_right() - pickupW;
625             } else if( position == "overlapping" ) {
626                 if( get_option<std::string>( "SIDEBAR_POSITION" ) == "right" ) {
627                     pickupX = TERMX - pickupW;
628                 }
629             }
630 
631             w_pickup = catacurses::newwin( pickupH, pickupW, point( pickupX, 0 ) );
632             w_item_info = catacurses::newwin( TERMY - pickupH, pickupW,
633                                               point( pickupX, pickupH ) );
634 
635             ui.position( point( pickupX, 0 ), point( pickupW, TERMY ) );
636         } );
637         ui.mark_resize();
638 
639         int itemcount = 0;
640 
641         std::string action;
642         int raw_input_char = ' ';
643         input_context ctxt( "PICKUP", keyboard_mode::keychar );
644         ctxt.register_action( "UP" );
645         ctxt.register_action( "DOWN" );
646         ctxt.register_action( "PAGE_UP", to_translation( "Fast scroll up" ) );
647         ctxt.register_action( "PAGE_DOWN", to_translation( "Fast scroll down" ) );
648         ctxt.register_action( "RIGHT" );
649         ctxt.register_action( "LEFT" );
650         ctxt.register_action( "NEXT_TAB", to_translation( "Next page" ) );
651         ctxt.register_action( "PREV_TAB", to_translation( "Previous page" ) );
652         ctxt.register_action( "SCROLL_ITEM_INFO_UP" );
653         ctxt.register_action( "SCROLL_ITEM_INFO_DOWN" );
654         ctxt.register_action( "CONFIRM" );
655         ctxt.register_action( "SELECT_ALL" );
656         ctxt.register_action( "QUIT", to_translation( "Cancel" ) );
657         ctxt.register_action( "ANY_INPUT" );
658         ctxt.register_action( "HELP_KEYBINDINGS" );
659         ctxt.register_action( "FILTER" );
660         ctxt.register_action( "SELECT" );
661 #if defined(__ANDROID__)
662         ctxt.allow_text_entry = true; // allow user to specify pickup amount
663 #endif
664 
665         bool update = true;
666         int iScrollPos = 0;
667 
668         std::string filter;
669         std::string new_filter;
670         // Indexes of items that match the filter
671         std::vector<int> matches;
672         bool filter_changed = true;
673 
674         units::mass weight_predict = 0_gram;
675         units::volume volume_predict = 0_ml;
676         units::length length_predict = 0_mm;
677         units::volume ind_vol_predict = 0_ml;
678 
679         const std::string all_pickup_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ:;";
680 
681         ui.on_redraw( [&]( const ui_adaptor & ) {
682             const item &selected_item = *stacked_here[matches[selected]].front();
683 
684             if( selected >= 0 && selected <= static_cast<int>( stacked_here.size() ) - 1 ) {
685                 std::vector<iteminfo> vThisItem;
686                 selected_item.info( true, vThisItem );
687 
688                 item_info_data dummy( {}, {}, vThisItem, {}, iScrollPos );
689                 dummy.without_getch = true;
690                 dummy.without_border = true;
691 
692                 draw_item_info( w_item_info, dummy );
693             } else {
694                 werase( w_item_info );
695                 wnoutrefresh( w_item_info );
696             }
697             draw_custom_border( w_item_info, 0 );
698 
699             // print info window title: < item name >
700             mvwprintw( w_item_info, point( 2, 0 ), "< " );
701             trim_and_print( w_item_info, point( 4, 0 ), pickupW - 8, selected_item.color_in_inventory(),
702                             selected_item.display_name() );
703             wprintw( w_item_info, " >" );
704             wnoutrefresh( w_item_info );
705 
706             const std::string pickup_chars = ctxt.get_available_single_char_hotkeys( all_pickup_chars );
707 
708             werase( w_pickup );
709             pickup_rect::list.clear();
710             for( int cur_it = start; cur_it < start + maxitems; cur_it++ ) {
711                 if( cur_it < static_cast<int>( matches.size() ) ) {
712                     int true_it = matches[cur_it];
713                     const item &this_item = *stacked_here[true_it].front();
714                     nc_color icolor = this_item.color_in_inventory();
715                     if( cur_it == selected ) {
716                         icolor = hilite( c_white );
717                     }
718 
719                     if( cur_it < static_cast<int>( pickup_chars.size() ) ) {
720                         mvwputch( w_pickup, point( 0, 2 + ( cur_it % maxitems ) ), icolor,
721                                   static_cast<char>( pickup_chars[cur_it] ) );
722                     } else if( cur_it < static_cast<int>( pickup_chars.size() ) + static_cast<int>
723                                ( pickup_chars.size() ) *
724                                static_cast<int>( pickup_chars.size() ) ) {
725                         int p = cur_it - pickup_chars.size();
726                         int p1 = p / pickup_chars.size();
727                         int p2 = p % pickup_chars.size();
728                         mvwprintz( w_pickup, point( 0, 2 + ( cur_it % maxitems ) ), icolor, "`%c%c",
729                                    static_cast<char>( pickup_chars[p1] ), static_cast<char>( pickup_chars[p2] ) );
730                     } else {
731                         mvwputch( w_pickup, point( 0, 2 + ( cur_it % maxitems ) ), icolor, ' ' );
732                     }
733                     if( getitem[true_it].pick ) {
734                         if( getitem[true_it].count == 0 ) {
735                             wprintz( w_pickup, c_light_blue, " + " );
736                         } else {
737                             wprintz( w_pickup, c_light_blue, " # " );
738                         }
739                     } else {
740                         wprintw( w_pickup, " - " );
741                     }
742                     std::string item_name;
743                     if( stacked_here[true_it].front()->is_money() ) {
744                         //Count charges
745                         // TODO: transition to the item_location system used for the inventory
746                         unsigned int charges_total = 0;
747                         for( const item_stack::iterator &it : stacked_here[true_it] ) {
748                             charges_total += it->ammo_remaining();
749                         }
750                         //Picking up none or all the cards in a stack
751                         if( !getitem[true_it].pick || getitem[true_it].count == 0 ) {
752                             item_name = stacked_here[true_it].front()->display_money( stacked_here[true_it].size(),
753                                         charges_total );
754                         } else {
755                             unsigned int charges = 0;
756                             int c = getitem[true_it].count;
757                             for( std::list<item_stack::iterator>::iterator it = stacked_here[true_it].begin();
758                                  it != stacked_here[true_it].end() && c > 0; ++it, --c ) {
759                                 charges += ( *it )->ammo_remaining();
760                             }
761                             item_name = stacked_here[true_it].front()->display_money( getitem[true_it].count, charges_total,
762                                         charges );
763                         }
764                     } else {
765                         item_name = this_item.display_name( stacked_here[true_it].size() );
766                     }
767                     if( stacked_here[true_it].size() > 1 ) {
768                         item_name = string_format( "%d %s", stacked_here[true_it].size(), item_name );
769                     }
770                     if( get_option<bool>( "ITEM_SYMBOLS" ) ) {
771                         item_name = string_format( "%s %s", this_item.symbol().c_str(),
772                                                    item_name );
773                     }
774 
775                     // if the item does not belong to your fraction then add the stolen symbol
776                     if( !this_item.is_owned_by( player_character, true ) ) {
777                         item_name = string_format( "<color_light_red>!</color> %s", item_name );
778                     }
779 
780                     int y = 2 + ( cur_it % maxitems );
781                     trim_and_print( w_pickup, point( 6, y ), pickupW - 6, icolor, item_name );
782                     pickup_rect rect = pickup_rect( point( 6, y ), point( pickupW - 1, y ) );
783                     rect.cur_it = cur_it;
784                     pickup_rect::list.push_back( rect );
785                 }
786             }
787 
788             mvwprintw( w_pickup, point( 0, maxitems + 2 ), _( "[%s] Unmark" ),
789                        ctxt.get_desc( "LEFT", 1 ) );
790 
791             center_print( w_pickup, maxitems + 2, c_light_gray, string_format( _( "[%s] Help" ),
792                           ctxt.get_desc( "HELP_KEYBINDINGS", 1 ) ) );
793 
794             right_print( w_pickup, maxitems + 2, 0, c_light_gray, string_format( _( "[%s] Mark" ),
795                          ctxt.get_desc( "RIGHT", 1 ) ) );
796 
797             mvwprintw( w_pickup, point( 0, maxitems + 3 ), _( "[%s] Prev" ),
798                        ctxt.get_desc( "PREV_TAB", 1 ) );
799 
800             center_print( w_pickup, maxitems + 3, c_light_gray, string_format( _( "[%s] All" ),
801                           ctxt.get_desc( "SELECT_ALL", 1 ) ) );
802 
803             right_print( w_pickup, maxitems + 3, 0, c_light_gray, string_format( _( "[%s] Next" ),
804                          ctxt.get_desc( "NEXT_TAB", 1 ) ) );
805 
806             const std::string fmted_weight_predict = colorize(
807                         string_format( "%.1f", round_up( convert_weight( weight_predict ), 1 ) ),
808                         weight_predict > player_character.weight_capacity() ? c_red : c_white );
809             const std::string fmted_weight_capacity = string_format(
810                         "%.1f", round_up( convert_weight( player_character.weight_capacity() ), 1 ) );
811             const std::string fmted_volume_predict = colorize(
812                         format_volume( volume_predict ),
813                         volume_predict > player_character.volume_capacity() ? c_red : c_white );
814             const std::string fmted_volume_capacity = format_volume( player_character.volume_capacity() );
815 
816             const std::string fmted_ind_volume_predict = colorize( format_volume( ind_vol_predict ),
817                     ind_vol_predict > player_character.max_single_item_volume() ? c_red : c_white );
818             const std::string fmted_ind_length_predict = colorize( string_format( "%.2f",
819                     convert_length_cm_in( length_predict ) ),
820                     length_predict > player_character.max_single_item_length() ? c_red : c_white );
821             const std::string fmted_ind_volume_capac = format_volume(
822                         player_character.max_single_item_volume() );
823             const units::length indiv = player_character.max_single_item_length();
824             const std::string fmted_ind_length_capac = string_format( "%.2f", convert_length_cm_in( indiv ) );
825 
826             trim_and_print( w_pickup, point_zero, pickupW, c_white,
827                             string_format( _( "PICK Wgt %1$s/%2$s  Vol %3$s/%4$s" ),
828                                            fmted_weight_predict, fmted_weight_capacity,
829                                            fmted_volume_predict, fmted_volume_capacity
830                                          ) );
831             trim_and_print( w_pickup, point_south, pickupW, c_white,
832                             string_format( _( "INDV Vol %1$s/%2$s  Lng %3$s/%4$s" ),
833                                            fmted_ind_volume_predict, fmted_ind_volume_capac,
834                                            fmted_ind_length_predict, fmted_ind_length_capac ) );
835 
836             wnoutrefresh( w_pickup );
837         } );
838 
839         // Now print the two lists; those on the ground and about to be added to inv
840         // Continue until we hit return or space
841         do {
842             const std::string pickup_chars = ctxt.get_available_single_char_hotkeys( all_pickup_chars );
843             // -2 lines for border, -2 to preserve a line at top/bottom for context
844             const int scroll_lines = catacurses::getmaxy( w_item_info ) - 4;
845             int idx = -1;
846             const int recmax = static_cast<int>( matches.size() );
847             const int scroll_rate = recmax > 20 ? 10 : 3;
848 
849             if( action == "ANY_INPUT" &&
850                 raw_input_char >= '0' && raw_input_char <= '9' ) {
851                 int raw_input_char_value = static_cast<char>( raw_input_char ) - '0';
852                 itemcount *= 10;
853                 itemcount += raw_input_char_value;
854                 if( itemcount < 0 ) {
855                     itemcount = 0;
856                 }
857             } else if( action == "SELECT" ) {
858                 cata::optional<point> pos = ctxt.get_coordinates_text( w_pickup );
859                 if( pos ) {
860                     if( window_contains_point_relative( w_pickup, pos.value() ) ) {
861                         pickup_rect *rect = pickup_rect::find_by_coordinate( pos.value() );
862                         if( rect != nullptr ) {
863                             selected = rect->cur_it;
864                             iScrollPos = 0;
865                             idx = selected;
866                         }
867                     }
868                 }
869 
870             } else if( action == "SCROLL_ITEM_INFO_UP" ) {
871                 iScrollPos -= scroll_lines;
872             } else if( action == "SCROLL_ITEM_INFO_DOWN" ) {
873                 iScrollPos += scroll_lines;
874             } else if( action == "PREV_TAB" ) {
875                 if( start > 0 ) {
876                     start -= maxitems;
877                 } else {
878                     start = static_cast<int>( ( matches.size() - 1 ) / maxitems ) * maxitems;
879                 }
880                 selected = start;
881             } else if( action == "NEXT_TAB" ) {
882                 if( start + maxitems < recmax ) {
883                     start += maxitems;
884                 } else {
885                     start = 0;
886                 }
887                 iScrollPos = 0;
888                 selected = start;
889             } else if( action == "UP" ) {
890                 selected--;
891                 iScrollPos = 0;
892                 if( selected < 0 ) {
893                     selected = matches.size() - 1;
894                     start = static_cast<int>( matches.size() / maxitems ) * maxitems;
895                     if( start >= recmax ) {
896                         start -= maxitems;
897                     }
898                 } else if( selected < start ) {
899                     start -= maxitems;
900                 }
901             } else if( action == "DOWN" ) {
902                 selected++;
903                 iScrollPos = 0;
904                 if( selected >= recmax ) {
905                     selected = 0;
906                     start = 0;
907                 } else if( selected >= start + maxitems ) {
908                     start += maxitems;
909                 }
910             } else if( action == "PAGE_DOWN" ) {
911                 if( selected == recmax - 1 ) {
912                     selected = 0;
913                     start = 0;
914                 } else if( selected + scroll_rate >= recmax ) {
915                     selected = recmax - 1;
916                     if( selected >= start + maxitems ) {
917                         start += maxitems;
918                     }
919                 } else {
920                     selected += +scroll_rate;
921                     iScrollPos = 0;
922                     if( selected >= recmax ) {
923                         selected = 0;
924                         start = 0;
925                     } else if( selected >= start + maxitems ) {
926                         start += maxitems;
927                     }
928                 }
929             } else if( action == "PAGE_UP" ) {
930                 if( selected == 0 ) {
931                     selected = recmax - 1;
932                     start = static_cast<int>( matches.size() / maxitems ) * maxitems;
933                     if( start >= recmax ) {
934                         start -= maxitems;
935                     }
936                 } else if( selected <= scroll_rate ) {
937                     selected = 0;
938                     start = 0;
939                 } else {
940                     selected += -scroll_rate;
941                     iScrollPos = 0;
942                     if( selected < start ) {
943                         start -= maxitems;
944                     }
945                 }
946             } else if( selected >= 0 && selected < recmax &&
947                        ( ( action == "RIGHT" && !getitem[matches[selected]].pick ) ||
948                          ( action == "LEFT" && getitem[matches[selected]].pick ) ) ) {
949                 idx = selected;
950             } else if( action == "FILTER" ) {
951                 new_filter = filter;
952                 string_input_popup popup;
953                 popup
954                 .title( _( "Set filter" ) )
955                 .width( 30 )
956                 .edit( new_filter );
957                 if( !popup.canceled() ) {
958                     filter_changed = true;
959                 }
960             } else if( action == "ANY_INPUT" && raw_input_char == '`' ) {
961                 std::string ext = string_input_popup()
962                                   .title( _( "Enter 2 letters (case sensitive):" ) )
963                                   .width( 3 )
964                                   .max_length( 2 )
965                                   .query_string();
966                 if( ext.size() == 2 ) {
967                     int p1 = pickup_chars.find( ext.at( 0 ) );
968                     int p2 = pickup_chars.find( ext.at( 1 ) );
969                     if( p1 != -1 && p2 != -1 ) {
970                         idx = pickup_chars.size() + ( p1 * pickup_chars.size() ) + p2;
971                     }
972                 }
973             } else if( action == "ANY_INPUT" ) {
974                 idx = ( raw_input_char <= 127 ) ? pickup_chars.find( raw_input_char ) : -1;
975                 iScrollPos = 0;
976             } else if( action == "SELECT_ALL" ) {
977                 int count = 0;
978                 for( int i : matches ) {
979                     if( getitem[i].pick ) {
980                         count++;
981                     }
982                     getitem[i].pick = true;
983                 }
984                 if( count == static_cast<int>( stacked_here.size() ) ) {
985                     for( size_t i = 0; i < stacked_here.size(); i++ ) {
986                         getitem[i].pick = false;
987                     }
988                 }
989                 update = true;
990             }
991 
992             if( idx >= 0 && idx < static_cast<int>( matches.size() ) ) {
993                 size_t true_idx = matches[idx];
994                 if( itemcount != 0 || getitem[true_idx].count == 0 ) {
995                     const item &temp = *stacked_here[true_idx].front();
996                     int amount_available = temp.count_by_charges() ? temp.charges : stacked_here[true_idx].size();
997                     if( itemcount >= amount_available ) {
998                         itemcount = 0;
999                     }
1000                     getitem[true_idx].count = itemcount;
1001                     itemcount = 0;
1002                 }
1003 
1004                 // Note: this might not change the value of getitem[idx] at all!
1005                 getitem[true_idx].pick = ( action == "RIGHT" ? true :
1006                                            ( action == "LEFT" ? false :
1007                                              !getitem[true_idx].pick ) );
1008                 if( action != "RIGHT" && action != "LEFT" ) {
1009                     selected = idx;
1010                     start = static_cast<int>( idx / maxitems ) * maxitems;
1011                 }
1012 
1013                 if( !getitem[true_idx].pick ) {
1014                     getitem[true_idx].count = 0;
1015                 }
1016                 update = true;
1017             }
1018             if( filter_changed ) {
1019                 matches.clear();
1020                 while( matches.empty() ) {
1021                     auto filter_func = item_filter_from_string( new_filter );
1022                     for( size_t index = 0; index < stacked_here.size(); index++ ) {
1023                         if( filter_func( *stacked_here[index].front() ) ) {
1024                             matches.push_back( index );
1025                         }
1026                     }
1027                     if( matches.empty() ) {
1028                         popup( _( "Your filter returned no results" ) );
1029                         // The filter must have results, or simply be emptied or canceled,
1030                         // as this screen can't be reached without there being
1031                         // items available
1032                         string_input_popup popup;
1033                         popup
1034                         .title( _( "Set filter" ) )
1035                         .width( 30 )
1036                         .edit( new_filter );
1037                         if( popup.canceled() ) {
1038                             new_filter = filter;
1039                             filter_changed = false;
1040                         }
1041                     }
1042                 }
1043                 if( filter_changed ) {
1044                     filter = new_filter;
1045                     filter_changed = false;
1046                     selected = 0;
1047                     start = 0;
1048                     iScrollPos = 0;
1049                 }
1050             }
1051 
1052             if( update ) { // Update weight & volume information
1053                 update = false;
1054                 units::mass weight_picked_up = 0_gram;
1055                 units::volume volume_picked_up = 0_ml;
1056                 units::length length_picked_up = 0_mm;
1057                 for( size_t i = 0; i < getitem.size(); i++ ) {
1058                     if( getitem[i].pick ) {
1059                         // Make a copy for calculating weight/volume
1060                         item temp = *stacked_here[i].front();
1061                         if( temp.count_by_charges() && getitem[i].count < temp.charges && getitem[i].count != 0 ) {
1062                             temp.charges = getitem[i].count;
1063                         }
1064                         int num_picked = std::min( stacked_here[i].size(),
1065                                                    getitem[i].count == 0 ? stacked_here[i].size() : getitem[i].count );
1066                         weight_picked_up += temp.weight() * num_picked;
1067                         volume_picked_up += temp.volume() * num_picked;
1068                         length_picked_up = temp.length();
1069                     }
1070                 }
1071 
1072                 weight_predict = player_character.weight_carried() + weight_picked_up;
1073                 volume_predict = player_character.volume_carried() + volume_picked_up;
1074                 ind_vol_predict = volume_picked_up;
1075                 length_predict = length_picked_up;
1076             }
1077 
1078             ui_manager::redraw();
1079             action = ctxt.handle_input();
1080             raw_input_char = ctxt.get_raw_input().get_first_input();
1081 
1082         } while( action != "QUIT" && action != "CONFIRM" );
1083 
1084         bool item_selected = false;
1085         // Check if we have selected an item.
1086         for( pickup_count selection : getitem ) {
1087             if( selection.pick ) {
1088                 item_selected = true;
1089             }
1090         }
1091         if( action != "CONFIRM" || !item_selected ) {
1092             add_msg( _( "Never mind." ) );
1093             g->reenter_fullscreen();
1094             return;
1095         }
1096     }
1097 
1098     // At this point we've selected our items, register an activity to pick them up.
1099     std::vector<std::pair<item_stack::iterator, int>> pick_values;
1100     for( size_t i = 0; i < stacked_here.size(); i++ ) {
1101         const pickup_count &selection = getitem[i];
1102         if( !selection.pick ) {
1103             continue;
1104         }
1105 
1106         const std::list<item_stack::iterator> &stack = stacked_here[i];
1107         // Note: items can be both charged and stacked
1108         // For robustness, let's assume they can be both in the same stack
1109         bool pick_all = selection.count == 0;
1110         int count = selection.count;
1111         for( const item_stack::iterator &it : stack ) {
1112             if( !pick_all && count == 0 ) {
1113                 break;
1114             }
1115 
1116             if( it->count_by_charges() ) {
1117                 int num_picked = std::min( it->charges, count );
1118                 pick_values.emplace_back( it, num_picked );
1119                 count -= num_picked;
1120             } else {
1121                 pick_values.emplace_back( it, 0 );
1122                 --count;
1123             }
1124         }
1125     }
1126 
1127     std::vector<item_location> target_items;
1128     std::vector<int> quantities;
1129     for( std::pair<item_stack::iterator, int> &iter_qty : pick_values ) {
1130         if( from_vehicle ) {
1131             target_items.emplace_back( vehicle_cursor( *veh, cargo_part ), &*iter_qty.first );
1132         } else {
1133             target_items.emplace_back( map_cursor( p ), &*iter_qty.first );
1134         }
1135         quantities.push_back( iter_qty.second );
1136     }
1137 
1138     player_character.assign_activity( player_activity( pickup_activity_actor( target_items, quantities,
1139                                       player_character.pos() ) ) );
1140     if( min == -1 ) {
1141         // Auto pickup will need to auto resume since there can be several of them on the stack.
1142         player_character.activity.auto_resume = true;
1143     }
1144 
1145     g->reenter_fullscreen();
1146 }
1147 
1148 //helper function for Pickup::pick_up
show_pickup_message(const PickupMap & mapPickup)1149 void show_pickup_message( const PickupMap &mapPickup )
1150 {
1151     for( const auto &entry : mapPickup ) {
1152         if( entry.second.first.invlet != 0 ) {
1153             add_msg( _( "You pick up: %d %s [%c]" ), entry.second.second,
1154                      entry.second.first.display_name( entry.second.second ), entry.second.first.invlet );
1155         } else if( entry.second.first.count_by_charges() ) {
1156             add_msg( _( "You pick up: %s" ), entry.second.first.display_name( entry.second.second ) );
1157         } else {
1158             add_msg( _( "You pick up: %d %s" ), entry.second.second,
1159                      entry.second.first.display_name( entry.second.second ) );
1160         }
1161     }
1162 }
1163 
cost_to_move_item(const Character & who,const item & it)1164 int Pickup::cost_to_move_item( const Character &who, const item &it )
1165 {
1166     // Do not involve inventory capacity, it's not like you put it in backpack
1167     int ret = 50;
1168     if( who.is_armed() ) {
1169         // No free hand? That will cost you extra
1170         ret += 20;
1171     }
1172     const int delta_weight = units::to_gram( it.weight() - who.weight_capacity() );
1173     // Is it too heavy? It'll take 10 moves per kg over limit
1174     ret += std::max( 0, delta_weight / 100 );
1175 
1176     // Keep it sane - it's not a long activity
1177     return std::min( 400, ret );
1178 }
1179 
1180 std::vector<Pickup::pickup_rect> Pickup::pickup_rect::list;
1181 
find_by_coordinate(const point & p)1182 Pickup::pickup_rect *Pickup::pickup_rect::find_by_coordinate( const point &p )
1183 {
1184     for( pickup_rect &rect : pickup_rect::list ) {
1185         if( rect.contains( p ) ) {
1186             return &rect;
1187         }
1188     }
1189     return nullptr;
1190 }
1191