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 ▭
1187 }
1188 }
1189 return nullptr;
1190 }
1191