/* * File: stash.cc * Summary: Classes tracking player stashes * Written by: Darshan Shaligram */ #include "AppHdr.h" #include "artefact.h" #include "chardump.h" #include "cio.h" #include "clua.h" #include "command.h" #include "coord.h" #include "describe.h" #include "directn.h" #include "food.h" #include "itemname.h" #include "itemprop.h" #include "files.h" #include "invent.h" #include "items.h" #include "kills.h" #include "libutil.h" #include "menu.h" #include "message.h" #include "misc.h" #include "mon-util.h" #include "mon-stuff.h" #include "notes.h" #include "options.h" #include "place.h" #include "shopping.h" #include "spl-book.h" #include "stash.h" #include "stuff.h" #include "env.h" #include "tags.h" #include "terrain.h" #include "traps.h" #include "travel.h" #include "tutorial.h" #include "viewgeom.h" #include #include #include #include #include // Global StashTracker StashTrack; #define ST_MAJOR_VER ((unsigned char) 4) #define ST_MINOR_VER ((unsigned char) 8) void stash_init_new_level() { // If there's an existing stash level for Pan, blow it away. StashTrack.remove_level( level_id(LEVEL_PANDEMONIUM) ); StashTrack.remove_level( level_id(LEVEL_PORTAL_VAULT) ); } std::string userdef_annotate_item(const char *s, const item_def *item, bool exclusive) { #ifdef CLUA_BINDINGS if (exclusive) lua_set_exclusive_item(item); std::string ann; if (!clua.callfn(s, "u>s", item, &ann) && !clua.error.empty()) mprf(MSGCH_ERROR, "Lua error: %s", clua.error.c_str()); if (exclusive) lua_set_exclusive_item(NULL); return (ann); #else return (""); #endif } std::string stash_annotate_item(const char *s, const item_def *item, bool exclusive = false) { std::string text = userdef_annotate_item(s, item, exclusive); if (item->base_type == OBJ_BOOKS && item_type_known(*item) && item->sub_type != BOOK_MANUAL && item->sub_type != BOOK_DESTRUCTION || count_staff_spells(*item, true) > 1) { formatted_string fs; item_def dup = *item; spellbook_contents(dup, item->base_type == OBJ_BOOKS ? RBOOK_READ_SPELL : RBOOK_USE_STAFF, &fs); text += EOL; text += fs.tostring(2, -2); } return text; } void maybe_update_stashes() { if (Options.stash_tracking && !crawl_state.arena) { StashTrack.update_visible_stashes( Options.stash_tracking == STM_ALL ? StashTracker::ST_AGGRESSIVE : StashTracker::ST_PASSIVE); } } bool is_stash(int x, int y) { LevelStashes *ls = StashTrack.find_current_level(); if (ls) { Stash *s = ls->find_stash(x, y); return (s && s->enabled); } return (false); } std::string get_stash_desc(int x, int y) { LevelStashes *ls = StashTrack.find_current_level(); if (ls) { Stash *s = ls->find_stash(x, y); if (s) { const std::string desc = s->description(); if (!desc.empty()) return ("[Stash: " + desc + "]"); } } return ""; } void describe_stash(int x, int y) { std::string desc = get_stash_desc(x, y); if (!desc.empty()) mpr(desc.c_str(), MSGCH_EXAMINE_FILTER); } std::vector Stash::get_items() const { return items; } std::vector item_list_in_stash( coord_def pos ) { std::vector ret; LevelStashes *ls = StashTrack.find_current_level(); if (ls) { Stash *s = ls->find_stash(pos.x, pos.y); if (s) ret = s->get_items(); } return ret; } static void _fully_identify_item(item_def *item) { if (!item || !item->is_valid()) return; set_ident_flags( *item, ISFLAG_IDENT_MASK ); if (item->base_type != OBJ_WEAPONS) set_ident_type( *item, ID_KNOWN_TYPE ); } // ---------------------------------------------------------------------- // Stash // ---------------------------------------------------------------------- bool Stash::aggressive_verify = true; std::vector Stash::filters; Stash::Stash(int xp, int yp) : enabled(true), items() { // First, fix what square we're interested in if (xp == -1) { xp = you.pos().x; yp = you.pos().y; } x = (unsigned char) xp; y = (unsigned char) yp; abspos = GXM * (int) y + x; update(); } bool Stash::are_items_same(const item_def &a, const item_def &b) { const bool same = a.base_type == b.base_type && a.sub_type == b.sub_type && a.plus == b.plus && a.plus2 == b.plus2 && a.special == b.special && a.colour == b.colour && a.flags == b.flags && a.quantity == b.quantity; // Account for rotting meat when comparing items. return (same || (a.base_type == b.base_type && (a.base_type == OBJ_CORPSES || (a.base_type == OBJ_FOOD && a.sub_type == FOOD_CHUNK && b.sub_type == FOOD_CHUNK)) && a.plus == b.plus)); } void Stash::filter(const std::string &str) { std::string base = str; unsigned char subc = 255; std::string subs = ""; std::string::size_type cpos = base.find(":", 0); if (cpos != std::string::npos) { subc = atoi(subs.c_str()); base = base.substr(0, cpos); } const int base_num = atoi(base.c_str()); if (base_num == 0 && base != "0" || subc == 0 && subs != "0") { item_types_pair pair = item_types_by_name(str); if (pair.base_type == OBJ_UNASSIGNED) { Options.report_error("Invalid stash filter '" + str + "'"); return; } filter(pair.base_type, pair.sub_type); } else { const object_class_type basec = static_cast(base_num); filter(basec, subc); } } void Stash::filter(object_class_type base, unsigned char sub) { item_def item; item.base_type = base; item.sub_type = sub; filters.push_back(item); } bool Stash::is_filtered(const item_def &item) { for (int i = 0, count = filters.size(); i < count; ++i) { const item_def &filter = filters[i]; if (item.base_type == filter.base_type && (filter.sub_type == 255 || item.sub_type == filter.sub_type)) { if (is_artefact(item)) return (false); if (filter.sub_type != 255 && !item_type_known(item)) return (false); return (true); } } return (false); } bool Stash::unverified() const { return (!verified); } bool Stash::pickup_eligible() const { for (int i = 0, size = items.size(); i < size; ++i) if (item_needs_autopickup(items[i])) return (true); return (false); } bool Stash::is_boring_feature(dungeon_feature_type feat) { switch (feat) { // Discard spammy dungeon features. case DNGN_SHALLOW_WATER: case DNGN_DEEP_WATER: case DNGN_LAVA: case DNGN_OPEN_DOOR: case DNGN_STONE_STAIRS_DOWN_I: case DNGN_STONE_STAIRS_DOWN_II: case DNGN_STONE_STAIRS_DOWN_III: case DNGN_STONE_STAIRS_UP_I: case DNGN_STONE_STAIRS_UP_II: case DNGN_STONE_STAIRS_UP_III: case DNGN_ESCAPE_HATCH_DOWN: case DNGN_ESCAPE_HATCH_UP: case DNGN_ENTER_SHOP: case DNGN_ABANDONED_SHOP: case DNGN_UNDISCOVERED_TRAP: return (true); default: return (feat_is_solid(feat)); } } static bool _grid_has_mimic_item(const coord_def& pos) { const monsters *mon = monster_at(pos); return (mon && mons_is_unknown_mimic(mon)); } static bool _grid_has_perceived_item(const coord_def& pos) { return (you.visible_igrd(pos) != NON_ITEM || _grid_has_mimic_item(pos)); } static bool _grid_has_perceived_multiple_items(const coord_def& pos) { int count = 0; if (_grid_has_mimic_item(pos)) ++count; for (stack_iterator si(pos, true); si && count < 2; ++si) ++count; return (count > 1); } void Stash::update() { coord_def p(x,y); feat = grd(p); trap = NUM_TRAPS; if (is_boring_feature(feat)) feat = DNGN_FLOOR; if (feat_is_trap(feat)) trap = get_trap_type(p); // If this is your position, you know what's on this square if (p == you.pos()) { // Zap existing items items.clear(); // Now, grab all items on that square and fill our vector for (stack_iterator si(p, true); si; ++si) if (!is_filtered(*si)) add_item(*si); verified = true; } // If this is not your position, the only thing we can do is verify that // what the player sees on the square is the first item in this vector. else { if (!_grid_has_perceived_item(p)) { items.clear(); verified = true; return; } // There's something on this square. Take a squint at it. const item_def *pitem; if (_grid_has_mimic_item(p)) pitem = &get_mimic_item(monster_at(p)); else { pitem = &mitm[you.visible_igrd(p)]; tutorial_first_item(*pitem); } const item_def& item = *pitem; if (!_grid_has_perceived_multiple_items(p)) items.clear(); // We knew of nothing on this square, so we'll assume this is the // only item here, but mark it as unverified unless we can see nothing // under the item. if (items.size() == 0) { if (!is_filtered(item)) add_item(item); // Note that we could be lying here, since we can have // a verified falsehood (if there's a mimic.) verified = !_grid_has_perceived_multiple_items(p); return; } // There's more than one item in this pile. As long as the top item is // not filtered, we can check to see if it matches what we think the // top item is. if (is_filtered(item)) return; const item_def &first = items[0]; // Compare these items if (!are_items_same(first, item)) { if (aggressive_verify) { // See if 'item' matches any of the items we have. If it does, // we'll just make that the first item and leave 'verified' // unchanged. // Start from 1 because we've already checked items[0] for (int i = 1, count = items.size(); i < count; ++i) { if (are_items_same(items[i], item)) { // Found it. Swap it to the front of the vector. std::swap(items[i], items[0]); // We don't set verified to true. If this stash was // already unverified, it remains so. return; } } } // If this is unverified, forget last item on stack. This isn't // terribly clever, but it prevents the vector swelling forever. if (!verified) items.pop_back(); // Items are different. We'll put this item in the front of our // vector, and mark this as unverified add_item(item, true); verified = false; } } } static bool _is_rottable(const item_def &item) { return (item.base_type == OBJ_CORPSES || (item.base_type == OBJ_FOOD && item.sub_type == FOOD_CHUNK)); } static short _min_rot(const item_def &item) { if (item.base_type == OBJ_FOOD) return 0; if (item.base_type == OBJ_CORPSES && item.sub_type == CORPSE_SKELETON) return 0; if (!mons_skeleton(item.plus)) return 0; else return -200; } // Returns the item name for a given item, with any appropriate // stash-tracking pre/suffixes. std::string Stash::stash_item_name(const item_def &item) { std::string name = item.name(DESC_NOCAP_A); if (!_is_rottable(item)) return name; if (item.plus2 <= _min_rot(item)) { name += " (gone by now)"; return name; } // Skeletons show no signs of rotting before they're gone if (item.base_type == OBJ_CORPSES && item.sub_type == CORPSE_SKELETON) return name; // Item was already seen to be rotten if (item.special < 100) return name; if (item.plus2 <= 0) name += " (skeletalised by now)"; else if (item.plus2 < 100) name += " (rotten by now)"; return name; } class StashMenu : public InvMenu { public: StashMenu() : InvMenu(MF_SINGLESELECT), can_travel(false) { set_type(MT_PICKUP); set_tag("stash"); // override "inventory" tag } unsigned char getkey() const; public: bool can_travel; protected: void draw_title(); bool process_key(int key); }; void StashMenu::draw_title() { if (title) { cgotoxy(1, 1); textcolor(title->colour); cprintf( "%s", title->text.c_str()); if (title->quantity) { cprintf(", %d item%s", title->quantity, title->quantity == 1? "" : "s"); } cprintf(")"); if (action_cycle == Menu::CYCLE_TOGGLE) { cprintf(" [a-z: %s ?/!: %s]", menu_action == ACT_EXAMINE ? "examine" : "shopping", menu_action == ACT_EXAMINE ? "shopping" : "examine"); } if (can_travel) { if (action_cycle == Menu::CYCLE_TOGGLE) { // XXX: This won't fit in the title, so it goes into the // footer/-more-. Not ideal, but I don't know where else // to put it. std::string str = "[ENTER: travel]"; set_more(formatted_string::parse_string(str)); flags |= MF_ALWAYS_SHOW_MORE; } else cprintf(" [ENTER: travel]"); } } } bool StashMenu::process_key(int key) { if (key == CK_ENTER) { // Travel activates. lastch = 1; return (false); } return Menu::process_key(key); } unsigned char StashMenu::getkey() const { return (lastch); } static MenuEntry *stash_menu_fixup(MenuEntry *me) { const item_def *item = static_cast( me->data ); if (item->base_type == OBJ_GOLD) { me->quantity = 0; me->colour = DARKGREY; } return (me); } bool Stash::show_menu(const level_pos &prefix, bool can_travel) const { const std::string prefix_str = short_place_name(prefix.id); StashMenu menu; MenuEntry *mtitle = new MenuEntry("Stash (" + prefix_str, MEL_TITLE); menu.can_travel = can_travel; mtitle->quantity = items.size(); menu.set_title(mtitle); menu.load_items( InvMenu::xlat_itemvect(items), stash_menu_fixup); std::vector sel; while (true) { sel = menu.show(); if (menu.getkey() == 1) return (true); if (sel.size() != 1) break; item_def *item = static_cast( sel[0]->data ); describe_item(*item); } return (false); } std::string Stash::description() const { if (!enabled || items.empty()) return (""); const item_def &item = items[0]; std::string desc = stash_item_name(item); size_t sz = items.size(); if (sz > 1) { char additionals[50]; snprintf(additionals, sizeof additionals, " (...%ld)", (unsigned long) (sz - 1)); desc += additionals; } return (desc); } std::string Stash::feature_description() const { if (feat == DNGN_FLOOR) return (""); return (::feature_description(feat, trap)); } bool Stash::matches_search(const std::string &prefix, const base_pattern &search, stash_search_result &res) const { if (!enabled || items.empty() && feat == DNGN_FLOOR) return (false); for (unsigned i = 0; i < items.size(); ++i) { const item_def &item = items[i]; std::string s = stash_item_name(item); std::string ann = stash_annotate_item(STASH_LUA_SEARCH_ANNOTATE, &item); if (search.matches(prefix + " " + ann + s)) { if (!res.count++) res.match = s; res.matches += item.quantity; continue; } if (is_dumpable_artefact(item, false)) { std::string desc = munge_description(get_item_description(item, false, true)); if (search.matches(desc)) { if (!res.count++) res.match = s; res.matches += item.quantity; } } } if (!res.matches && feat != DNGN_FLOOR) { const std::string fdesc = feature_description(); if (!fdesc.empty() && search.matches(fdesc)) { res.match = fdesc; res.matches = 1; } } if (res.matches) { res.stash = this; // XXX pos.pos looks lame. Lameness is not solicited. res.pos.pos.x = x; res.pos.pos.y = y; } return !!res.matches; } void Stash::_update_corpses(long rot_time) { for (int i = items.size() - 1; i >= 0; i--) { item_def &item = items[i]; if (!_is_rottable(item)) continue; long new_rot = static_cast(item.plus2) - rot_time; if (new_rot <= _min_rot(item)) { items.erase(items.begin() + i); continue; } item.plus2 = static_cast(new_rot); } } void Stash::add_item(const item_def &item, bool add_to_front) { if (_is_rottable(item)) StashTrack.update_corpses(); if (add_to_front) items.insert(items.begin(), item); else items.push_back(item); seen_item(item); if (!_is_rottable(item)) return; // item.special remains unchanged in the stash, to show how fresh it // was when last seen. It's plus2 that's decayed over time. if (add_to_front) { item_def &it = items.front(); it.plus2 = it.special; } else { item_def &it = items.back(); it.plus2 = it.special; } } void Stash::write(std::ostream &os, int refx, int refy, std::string place, bool identify) const { if (!enabled || (items.size() == 0 && verified)) return; bool note_status = notes_are_active(); activate_notes(false); os << "(" << ((int) x - refx) << ", " << ((int) y - refy) << (place.length()? ", " + place : "") << ")" << std::endl; char buf[ITEMNAME_SIZE]; for (int i = 0; i < (int) items.size(); ++i) { item_def item = items[i]; if (identify) _fully_identify_item(&item); std::string s = stash_item_name(item); strncpy(buf, s.c_str(), sizeof buf); std::string ann = userdef_annotate_item(STASH_LUA_DUMP_ANNOTATE, &item); if (!ann.empty()) { trim_string(ann); ann = " " + ann; } os << " " << buf << (!ann.empty()? ann : std::string()) << (!verified && (items.size() > 1 || i) ? " (still there?)" : "") << std::endl; if (is_dumpable_artefact(item, false)) { std::string desc = munge_description(get_item_description(item, false, true)); // Kill leading and trailing whitespace desc.erase(desc.find_last_not_of(" \n\t") + 1); desc.erase(0, desc.find_first_not_of(" \n\t")); // If string is not-empty, pad out to a neat indent if (desc.length()) { // Walk backwards and prepend indenting spaces to \n characters. for (int j = desc.length() - 1; j >= 0; --j) if (desc[j] == '\n') desc.insert(j + 1, " "); os << " " << desc << std::endl; } } } if (items.size() <= 1 && !verified) os << " (unseen)" << std::endl; activate_notes(note_status); } void Stash::save(writer& outf) const { // How many items on this square? marshallShort(outf, (short) items.size()); marshallByte(outf, x); marshallByte(outf, y); marshallByte(outf, feat); marshallByte(outf, trap); // Note: Enabled save value is inverted logic, so that it defaults to true marshallByte(outf, (unsigned char) ((verified? 1 : 0) | (!enabled? 2 : 0)) ); // And dump the items individually. We don't bother saving fields we're // not interested in (and don't anticipate being interested in). for (unsigned i = 0; i < items.size(); ++i) marshallItem(outf, items[i]); } void Stash::load(reader& inf) { // How many items? int count = unmarshallShort(inf); x = unmarshallByte(inf); y = unmarshallByte(inf); feat = static_cast( static_cast( unmarshallByte(inf) )); trap = static_cast( static_cast( unmarshallByte(inf) )); unsigned char flags = unmarshallByte(inf); verified = (flags & 1) != 0; // Note: Enabled save value is inverted so it defaults to true. enabled = (flags & 2) == 0; abspos = GXM * (int) y + x; // Zap out item vector, in case it's in use (however unlikely) items.clear(); // Read in the items for (int i = 0; i < count; ++i) { item_def item; unmarshallItem(inf, item); items.push_back(item); } } std::ostream &operator << (std::ostream &os, const Stash &s) { s.write(os); return os; } ShopInfo::ShopInfo(int xp, int yp) : x(xp), y(yp), name(), shoptype(-1), visited(false), items() { // Most of our initialization will be done externally; this class is really // a mildly glorified struct. const shop_struct *sh = get_shop(coord_def(x, y)); if (sh) shoptype = sh->type; } void ShopInfo::add_item(const item_def &sitem, unsigned price) { shop_item it; it.item = sitem; it.price = price; items.push_back(it); } std::string ShopInfo::shop_item_name(const shop_item &si) const { const unsigned long oldflags = si.item.flags; if (shoptype_identifies_stock(static_cast(this->shoptype))) const_cast(si).item.flags |= ISFLAG_IDENT_MASK; if (oldflags != si.item.flags) const_cast(si).item.flags = oldflags; return make_stringf("%s (%u gold)", Stash::stash_item_name(si.item).c_str(), si.price); } std::string ShopInfo::shop_item_desc(const shop_item &si) const { std::string desc; const unsigned long oldflags = si.item.flags; if (shoptype_identifies_stock(static_cast(this->shoptype))) const_cast(si).item.flags |= ISFLAG_IDENT_MASK; if (is_dumpable_artefact(si.item, false)) { desc = munge_description(get_item_description(si.item, false, true)); trim_string(desc); // Walk backwards and prepend indenting spaces to \n characters for (int i = desc.length() - 1; i >= 0; --i) if (desc[i] == '\n') desc.insert(i + 1, " "); } if (oldflags != si.item.flags) const_cast(si).item.flags = oldflags; return desc; } void ShopInfo::describe_shop_item(const shop_item &si) const { const unsigned long oldflags = si.item.flags; if (shoptype_identifies_stock(static_cast(this->shoptype))) const_cast(si).item.flags |= ISFLAG_IDENT_MASK; item_def it = static_cast(si.item); describe_item( it ); if (oldflags != si.item.flags) const_cast(si).item.flags = oldflags; } class ShopItemEntry : public InvEntry { public: ShopItemEntry(const ShopInfo::shop_item &it, const std::string &item_name, menu_letter hotkey) : InvEntry(it.item) { text = item_name; hotkeys[0] = hotkey; } }; void ShopInfo::fill_out_menu(StashMenu &menu, const level_pos &place) const { menu.clear(); menu_letter hotkey; for (int i = 0, count = items.size(); i < count; ++i) { ShopItemEntry *me = new ShopItemEntry(items[i], shop_item_name(items[i]), hotkey++); if (shopping_list.is_on_list(items[i].item, &place)) me->colour = LIGHTCYAN; menu.add_entry(me); } } bool ShopInfo::show_menu(const level_pos &place, bool can_travel) const { const std::string place_str = short_place_name(place.id); StashMenu menu; MenuEntry *mtitle = new MenuEntry(name + " (" + place_str, MEL_TITLE); menu.can_travel = can_travel; menu.action_cycle = Menu::CYCLE_TOGGLE; menu.menu_action = Menu::ACT_EXAMINE; mtitle->quantity = items.size(); menu.set_title(mtitle); if (items.empty()) { MenuEntry *me = new MenuEntry( visited? " (Shop is empty)" : " (Shop contents are unknown)", MEL_ITEM, 0, 0); me->colour = DARKGREY; menu.add_entry(me); } else fill_out_menu(menu, place); std::vector sel; while (true) { sel = menu.show(); if (menu.getkey() == 1) return (true); if (sel.size() != 1) break; const shop_item *item = static_cast( sel[0]->data ); if (menu.menu_action == Menu::ACT_EXAMINE) describe_shop_item(*item); else { if (shopping_list.is_on_list(item->item, &place)) shopping_list.del_thing(item->item, &place); else shopping_list.add_thing(item->item, item->price, &place); // If the shop has identical items (like stacks of food in a // food shop) then adding/removing one to the shopping list // will have the same effect on the others, so the other // identical items will need to be re-coloured. fill_out_menu(menu, place); } } return (false); } std::string ShopInfo::description() const { return (name); } bool ShopInfo::matches_search(const std::string &prefix, const base_pattern &search, stash_search_result &res) const { if (items.empty() && visited) return (false); bool note_status = notes_are_active(); activate_notes(false); bool match = false; for (unsigned i = 0; i < items.size(); ++i) { std::string sname = shop_item_name(items[i]); std::string ann = stash_annotate_item( STASH_LUA_SEARCH_ANNOTATE, &items[i].item, true ); bool thismatch = false; if (search.matches(prefix + " " + ann + sname)) thismatch = true; else { std::string desc = shop_item_desc(items[i]); if (search.matches(desc)) thismatch = true; } if (thismatch) { if (!res.count++) res.match = sname; res.matches++; } } if (!res.matches) { std::string shoptitle = prefix + " {shop} " + name; if (!visited && items.empty()) shoptitle += "*"; if (search.matches(shoptitle)) { match = true; res.match = name; } } if (match || res.matches) { res.shop = this; res.pos.pos.x = x; res.pos.pos.y = y; } activate_notes(note_status); return (match || res.matches); } void ShopInfo::write(std::ostream &os, bool identify) const { bool note_status = notes_are_active(); activate_notes(false); os << "[Shop] " << name << std::endl; if (items.size() > 0) { for (unsigned i = 0; i < items.size(); ++i) { shop_item item = items[i]; if (identify) _fully_identify_item(&item.item); os << " " << shop_item_name(item) << std::endl; std::string desc = shop_item_desc(item); if (desc.length() > 0) os << " " << desc << std::endl; } } else if (visited) os << " (Shop is empty)" << std::endl; else os << " (Shop contents are unknown)" << std::endl; activate_notes(note_status); } void ShopInfo::save(writer& outf) const { marshallShort(outf, shoptype); int mangledx = (short) x; if (!visited) mangledx |= 1024; marshallShort(outf, mangledx); marshallShort(outf, (short) y); marshallShort(outf, (short) items.size()); marshallString4(outf, name); for (unsigned i = 0; i < items.size(); ++i) { marshallItem(outf, items[i].item); marshallShort(outf, (short) items[i].price ); } } void ShopInfo::load(reader& inf) { shoptype = unmarshallShort(inf); x = unmarshallShort(inf); visited = !(x & 1024); x &= 0xFF; y = unmarshallShort(inf); int itemcount = unmarshallShort(inf); unmarshallString4(inf, name); for (int i = 0; i < itemcount; ++i) { shop_item item; unmarshallItem(inf, item.item); item.price = (unsigned) unmarshallShort(inf); items.push_back(item); } } std::ostream &operator << (std::ostream &os, const ShopInfo &s) { s.write(os); return os; } LevelStashes::LevelStashes() : m_place(level_id::current()), m_stashes(), m_shops() { } level_id LevelStashes::where() const { return m_place; } Stash *LevelStashes::find_stash(int x, int y) { if (x == -1 || y == -1) { x = you.pos().x; y = you.pos().y; } const int abspos = (GXM * y) + x; stashes_t::iterator st = m_stashes.find(abspos); return (st == m_stashes.end()? NULL : &st->second); } const Stash *LevelStashes::find_stash(int x, int y) const { if (x == -1 || y == -1) { x = you.pos().x; y = you.pos().y; } const int abspos = (GXM * y) + x; stashes_t::const_iterator st = m_stashes.find(abspos); return (st == m_stashes.end()? NULL : &st->second); } const ShopInfo *LevelStashes::find_shop(int x, int y) const { for (unsigned i = 0; i < m_shops.size(); ++i) if (m_shops[i].isAt(x, y)) return (&m_shops[i]); return (NULL); } bool LevelStashes::shop_needs_visit(int x, int y) const { const ShopInfo *shop = find_shop(x, y); return (shop && !shop->is_visited()); } bool LevelStashes::needs_visit(int x, int y) const { const Stash *s = find_stash(x, y); if (s && (s->unverified() || s->pickup_eligible())) return (true); return (shop_needs_visit(x, y)); } ShopInfo &LevelStashes::get_shop(int x, int y) { for (unsigned i = 0; i < m_shops.size(); ++i) { if (m_shops[i].isAt(x, y)) return m_shops[i]; } ShopInfo si(x, y); si.set_name(shop_name(coord_def(x, y))); m_shops.push_back(si); return get_shop(x, y); } // Updates the stash at (x,y). Returns true if there was a stash at (x,y), false // otherwise. bool LevelStashes::update_stash(int x, int y) { Stash *s = find_stash(x, y); if (s) { s->update(); if (s->empty()) kill_stash(*s); return (true); } return (false); } // Removes a Stash from the level. void LevelStashes::kill_stash(const Stash &s) { m_stashes.erase(s.abs_pos()); } void LevelStashes::no_stash(int x, int y) { Stash *s = find_stash(x, y); bool en = false; if (s) { en = s->enabled = !s->enabled; s->update(); if (s->empty()) kill_stash(*s); } else { Stash newStash(x, y); newStash.enabled = false; m_stashes[ newStash.abs_pos() ] = newStash; } mpr(en? "I'll no longer ignore what I see on this square." : "Ok, I'll ignore what I see on this square."); } void LevelStashes::add_stash(int x, int y) { Stash *s = find_stash(x, y); if (s) { s->update(); if (s->empty()) kill_stash(*s); } else { Stash new_stash(x, y); if (!new_stash.empty()) m_stashes[ new_stash.abs_pos() ] = new_stash; } } bool LevelStashes::is_current() const { return (m_place == level_id::current()); } std::string LevelStashes::level_name() const { return m_place.describe(true, true); } std::string LevelStashes::short_level_name() const { return m_place.describe(); } int LevelStashes::_num_enabled_stashes() const { int rawcount = m_stashes.size(); if (!rawcount) return (0); for (stashes_t::const_iterator iter = m_stashes.begin(); iter != m_stashes.end(); iter++) { if (!iter->second.enabled) --rawcount; } return rawcount; } void LevelStashes::get_matching_stashes( const base_pattern &search, std::vector &results) const { std::string lplace = "{" + m_place.describe() + "}"; for (stashes_t::const_iterator iter = m_stashes.begin(); iter != m_stashes.end(); iter++) { if (iter->second.enabled) { stash_search_result res; if (iter->second.matches_search(lplace, search, res)) { res.pos.id = m_place; results.push_back(res); } } } for (unsigned i = 0; i < m_shops.size(); ++i) { stash_search_result res; if (m_shops[i].matches_search(lplace, search, res)) { res.pos.id = m_place; results.push_back(res); } } } void LevelStashes::_update_corpses(long rot_time) { for (stashes_t::iterator iter = m_stashes.begin(); iter != m_stashes.end(); iter++) { iter->second._update_corpses(rot_time); } } void LevelStashes::write(std::ostream &os, bool identify) const { if (visible_stash_count() == 0) return; os << level_name() << std::endl; for (unsigned i = 0; i < m_shops.size(); ++i) m_shops[i].write(os, identify); if (m_stashes.size()) { const Stash &s = m_stashes.begin()->second; int refx = s.getX(), refy = s.getY(); std::string levname = short_level_name(); for (stashes_t::const_iterator iter = m_stashes.begin(); iter != m_stashes.end(); iter++) { iter->second.write(os, refx, refy, levname, identify); } } os << std::endl; } void LevelStashes::save(writer& outf) const { // How many stashes on this level? marshallShort(outf, (short) m_stashes.size()); m_place.save(outf); // And write the individual stashes for (stashes_t::const_iterator iter = m_stashes.begin(); iter != m_stashes.end(); iter++) { iter->second.save(outf); } marshallShort(outf, (short) m_shops.size()); for (unsigned i = 0; i < m_shops.size(); ++i) m_shops[i].save(outf); } void LevelStashes::load(reader& inf) { int size = unmarshallShort(inf); m_place.load(inf); m_stashes.clear(); for (int i = 0; i < size; ++i) { Stash s; s.load(inf); if (!s.empty()) m_stashes[ s.abs_pos() ] = s; } m_shops.clear(); int shopc = unmarshallShort(inf); for (int i = 0; i < shopc; ++i) { ShopInfo si(0, 0); si.load(inf); m_shops.push_back(si); } } std::ostream &operator << (std::ostream &os, const LevelStashes &ls) { ls.write(os); return os; } LevelStashes &StashTracker::get_current_level() { return (levels[level_id::current()]); } LevelStashes *StashTracker::find_level(const level_id &id) { stash_levels_t::iterator i = levels.find(id); return (i != levels.end()? &i->second : NULL); } LevelStashes *StashTracker::find_current_level() { if (is_level_untrackable()) return (NULL); return find_level(level_id::current()); } bool StashTracker::update_stash(int x, int y) { LevelStashes *lev = find_current_level(); if (lev) { bool res = lev->update_stash(x, y); if (!lev->stash_count()) remove_level(); return (res); } return (false); } void StashTracker::remove_level(const level_id &place) { levels.erase(place); } void StashTracker::no_stash(int x, int y) { if (is_level_untrackable()) return ; LevelStashes ¤t = get_current_level(); current.no_stash(x, y); if (!current.stash_count()) remove_level(); } void StashTracker::add_stash(int x, int y, bool verbose) { if (is_level_untrackable()) return ; LevelStashes ¤t = get_current_level(); current.add_stash(x, y); if (verbose) { Stash *s = current.find_stash(x, y); if (s && s->enabled) mpr("Added stash."); } if (!current.stash_count()) remove_level(); } void StashTracker::dump(const char *filename, bool identify) const { std::ofstream outf(filename); if (outf) { write(outf, identify); outf.close(); } } void StashTracker::write(std::ostream &os, bool identify) const { os << you.your_name << std::endl << std::endl; if (!levels.size()) os << " You have no stashes." << std::endl; else { for (stash_levels_t::const_iterator iter = levels.begin(); iter != levels.end(); iter++) { iter->second.write(os, identify); } } } void StashTracker::save(writer& outf) const { // Write version info first - major + minor marshallByte(outf, ST_MAJOR_VER); marshallByte(outf, ST_MINOR_VER); // Time of last corpse update. marshallFloat(outf, (float) last_corpse_update); // How many levels have we? marshallShort(outf, (short) levels.size()); // And ask each level to write itself to the tag stash_levels_t::const_iterator iter = levels.begin(); for ( ; iter != levels.end(); iter++) iter->second.save(outf); } void StashTracker::load(reader& inf) { // Check version. Compatibility isn't important, since stash-tracking // is non-critical. unsigned char major = unmarshallByte(inf), minor = unmarshallByte(inf); if (major != ST_MAJOR_VER || minor != ST_MINOR_VER) return ; // Time of last corpse update. last_corpse_update = (double) unmarshallFloat(inf); int count = unmarshallShort(inf); levels.clear(); for (int i = 0; i < count; ++i) { LevelStashes st; st.load(inf); if (st.stash_count()) levels[st.where()] = st; } } void StashTracker::update_visible_stashes( StashTracker::stash_update_mode mode) { if (is_level_untrackable()) return; LevelStashes *lev = find_current_level(); for (int cy = crawl_view.glos1.y; cy <= crawl_view.glos2.y; ++cy) for (int cx = crawl_view.glos1.x; cx <= crawl_view.glos2.x; ++cx) { if (!in_bounds(cx, cy) || !you.see_cell(coord_def(cx, cy))) continue; const dungeon_feature_type grid = grd[cx][cy]; if ((!lev || !lev->update_stash(cx, cy)) && mode == ST_AGGRESSIVE && (_grid_has_perceived_item(coord_def(cx,cy)) || !Stash::is_boring_feature(grid))) { if (!lev) lev = &get_current_level(); lev->add_stash(cx, cy); } if (grid == DNGN_ENTER_SHOP) get_shop(cx, cy); } if (lev && !lev->stash_count()) remove_level(); } #define SEARCH_SPAM_THRESHOLD 400 static std::string lastsearch; static input_history search_history(15); void StashTracker::show_stash_search_prompt() { std::vector opts; if (!lastsearch.empty()) opts.push_back( make_stringf("Enter for \"%s\"", lastsearch.c_str()) ); if (level_type_is_stash_trackable(you.level_type) && lastsearch != ".") { opts.push_back("? for help"); } std::string prompt_qual = comma_separated_line(opts.begin(), opts.end(), ", or ", ", or "); if (!prompt_qual.empty()) prompt_qual = " [" + prompt_qual + "]"; mprf(MSGCH_PROMPT, "Search for what%s?", prompt_qual.c_str()); // Push the cursor down to the next line. Newline on the prompt will not // do the trick on DOS. mpr("", MSGCH_PROMPT); } class stash_search_reader : public line_reader { public: stash_search_reader(char *buf, size_t sz, int wcol = get_number_of_cols()) : line_reader(buf, sz, wcol) { } protected: int process_key(int ch) { if (ch == '?' && !pos) { *buffer = 0; return (ch); } return line_reader::process_key(ch); } }; // helper for search_stashes struct compare_by_distance { bool operator()(const stash_search_result& lhs, const stash_search_result& rhs) { if (lhs.player_distance != rhs.player_distance) { // Sort by increasing distance return (lhs.player_distance < rhs.player_distance); } else if (lhs.matches != rhs.matches) { // Then by decreasing number of matches return (lhs.matches > rhs.matches); } else return (false); } }; // helper for search_stashes struct compare_by_name { bool operator()(const stash_search_result& lhs, const stash_search_result& rhs) { if (lhs.match != rhs.match) { // Sort by name return (lhs.match < rhs.match); } else if (lhs.player_distance != rhs.player_distance) { // Then sort by increasing distance return (lhs.player_distance < rhs.player_distance); } else if (lhs.matches != rhs.matches) { // Then by decreasing number of matches return (lhs.matches > rhs.matches); } else return (false); } }; void StashTracker::search_stashes() { char buf[400]; this->update_corpses(); stash_search_reader reader(buf, sizeof buf); bool validline = false; while (true) { show_stash_search_prompt(); int ret = reader.read_line(); if (!ret) { validline = true; break; } else if (ret == '?') { show_stash_search_help(); redraw_screen(); } else { break; } } mesclr(); if (!validline || (!*buf && !lastsearch.length())) return; std::string csearch = *buf? buf : lastsearch; std::string help = lastsearch; lastsearch = csearch; if (csearch == ".") { if (!level_type_is_stash_trackable(you.level_type)) { mpr("Cannot track items on this level."); return; } #if defined(REGEX_PCRE) || defined(REGEX_POSIX) #define RE_ESCAPE "\\" #else #define RE_ESCAPE "" #endif csearch = (RE_ESCAPE "{") + level_id::current().describe() + (RE_ESCAPE "}"); } std::vector results; base_pattern *search = NULL; text_pattern tpat( csearch, true ); search = &tpat; lua_text_pattern ltpat( csearch ); if (lua_text_pattern::is_lua_pattern(csearch)) search = <pat; if (!search->valid()) { mpr("Your search expression is invalid.", MSGCH_PLAIN); lastsearch = help; return ; } get_matching_stashes(*search, results); if (results.empty()) { mpr("Can't find anything matching that.", MSGCH_PLAIN); return; } if (results.size() > SEARCH_SPAM_THRESHOLD) { mpr("Too many matches; use a more specific search.", MSGCH_PLAIN); return; } bool sort_by_dist = true; while (true) { const char* sort_style; if (sort_by_dist) { std::sort(results.begin(), results.end(), compare_by_distance()); sort_style = "by dist"; } else { std::sort(results.begin(), results.end(), compare_by_name()); sort_style = "by name"; } const bool again = display_search_results(results, sort_style); if (!again) break; sort_by_dist = !sort_by_dist; } } void StashTracker::get_matching_stashes( const base_pattern &search, std::vector &results) const { stash_levels_t::const_iterator iter = levels.begin(); for ( ; iter != levels.end(); iter++) { iter->second.get_matching_stashes(search, results); if (results.size() > SEARCH_SPAM_THRESHOLD) return; } level_id curr = level_id::current(); for (unsigned i = 0; i < results.size(); ++i) { int ldist = level_distance(curr, results[i].pos.id); if (ldist == -1) ldist = 1000; results[i].player_distance = ldist; } } class StashSearchMenu : public Menu { public: StashSearchMenu(const char* sort_style_) : Menu(), can_travel(true), request_toggle_sort_method(false), sort_style(sort_style_) { } public: bool can_travel; bool request_toggle_sort_method; const char* sort_style; protected: bool process_key(int key); void draw_title(); }; void StashSearchMenu::draw_title() { if (title) { cgotoxy(1, 1); textcolor(title->colour); cprintf("%d %s%s, sorted %s", title->quantity, title->text.c_str(), title->quantity > 1? "es" : "", sort_style); draw_title_suffix(formatted_string::parse_string(make_stringf( " [a-z: %s ?/!: change action /: change sort]", menu_action == ACT_EXECUTE ? "travel" : "examine")), false); } } bool StashSearchMenu::process_key(int key) { if (key == '/') { request_toggle_sort_method = true; return (false); } return Menu::process_key(key); } // Returns true to request redisplay with a different sort method bool StashTracker::display_search_results( std::vector &results, const char* sort_style) { if (results.empty()) return (false); bool travelable = can_travel_interlevel(); StashSearchMenu stashmenu(sort_style); stashmenu.set_tag("stash"); stashmenu.can_travel = travelable; stashmenu.action_cycle = Menu::CYCLE_TOGGLE; stashmenu.menu_action = Menu::ACT_EXECUTE; std::string title = "match"; MenuEntry *mtitle = new MenuEntry(title, MEL_TITLE); // Abuse of the quantity field. mtitle->quantity = results.size(); stashmenu.set_title(mtitle); // Don't make a menu so tall that we recycle hotkeys on the same page. if (results.size() > 52 && (stashmenu.maxpagesize() > 52 || stashmenu.maxpagesize() == 0)) { stashmenu.set_maxpagesize(52); } menu_letter hotkey; for (unsigned i = 0; i < results.size(); ++i, ++hotkey) { stash_search_result &res = results[i]; std::ostringstream matchtitle; matchtitle << "[" << short_place_name(res.pos.id) << "] " << res.match; if (res.matches > 1 && res.count > 1) matchtitle << " (+" << (res.matches - 1) << ")"; MenuEntry *me = new MenuEntry(matchtitle.str(), MEL_ITEM, 1, hotkey); me->data = &res; if (res.shop && !res.shop->is_visited()) me->colour = CYAN; stashmenu.add_entry(me); } stashmenu.set_flags( MF_SINGLESELECT ); std::vector sel; while (true) { sel = stashmenu.show(); if (stashmenu.request_toggle_sort_method) return (true); if (sel.size() == 1 && stashmenu.menu_action == StashSearchMenu::ACT_EXAMINE) { stash_search_result *res = static_cast(sel[0]->data); bool dotravel = false; if (res->shop) { dotravel = res->shop->show_menu(res->pos, can_travel_to(res->pos.id)); } else if (res->stash) { dotravel = res->stash->show_menu(res->pos, can_travel_to(res->pos.id)); } if (dotravel && can_travel_to(res->pos.id)) { redraw_screen(); const travel_target lp = res->pos; start_translevel_travel(lp); return (false); } continue; } break; } redraw_screen(); if (sel.size() == 1 && stashmenu.menu_action == Menu::ACT_EXECUTE) { const stash_search_result *res = static_cast(sel[0]->data); const level_pos lp = res->pos; start_translevel_travel(lp); } return (false); } void StashTracker::update_corpses() { if (you.elapsed_time - last_corpse_update < 20.0) return; const long rot_time = static_cast((you.elapsed_time - last_corpse_update) / 20.0); last_corpse_update = you.elapsed_time; for (stash_levels_t::iterator iter = levels.begin(); iter != levels.end(); iter++) { iter->second._update_corpses(rot_time); } } ////////////////////////////////////////////// ST_ItemIterator::ST_ItemIterator() { m_stash_level_it = StashTrack.levels.begin(); new_level(); //(*this)++; } ST_ItemIterator::operator bool() const { return (m_item != NULL); } const item_def& ST_ItemIterator::operator *() const { return (*m_item); } const item_def* ST_ItemIterator::operator->() const { return (m_item); } const level_id &ST_ItemIterator::place() { return (m_place); } const ShopInfo* ST_ItemIterator::shop() { return (m_shop); } const unsigned ST_ItemIterator::price() { return (m_price); } const ST_ItemIterator& ST_ItemIterator::operator ++ () { m_item = NULL; m_shop = NULL; const LevelStashes &ls = m_stash_level_it->second; if (m_stash_it == ls.m_stashes.end()) { if (m_shop_it == ls.m_shops.end()) { m_stash_level_it++; if (m_stash_level_it == StashTrack.levels.end()) return (*this); new_level(); return (*this); } m_shop = &(*m_shop_it); if (m_shop_item_it != m_shop->items.end()) { const ShopInfo::shop_item &item = *m_shop_item_it++; m_item = &(item.item); ASSERT(m_item->is_valid()); m_price = item.price; return (*this); } m_shop_it++; if (m_shop_it != ls.m_shops.end()) m_shop_item_it = m_shop_it->items.begin(); ++(*this); } else { if (m_stash_item_it != m_stash_it->second.items.end()) { m_item = &(*m_stash_item_it++); ASSERT(m_item->is_valid()); return (*this); } m_stash_it++; if (m_stash_it == ls.m_stashes.end()) { ++(*this); return (*this); } m_stash_item_it = m_stash_it->second.items.begin(); ++(*this); } return (*this); } void ST_ItemIterator::new_level() { m_item = NULL; m_shop = NULL; m_price = 0; if (m_stash_level_it == StashTrack.levels.end()) return; const LevelStashes &ls = m_stash_level_it->second; m_place = ls.m_place; m_stash_it = ls.m_stashes.begin(); if (m_stash_it != ls.m_stashes.end()) { m_stash_item_it = m_stash_it->second.items.begin(); if (m_stash_item_it != m_stash_it->second.items.end()) { m_item = &(*m_stash_item_it++); ASSERT(m_item->is_valid()); } } m_shop_it = ls.m_shops.begin(); if (m_shop_it != ls.m_shops.end()) { const ShopInfo &si = *m_shop_it; m_shop_item_it = si.items.begin(); if (m_item == NULL && m_shop_item_it != si.items.end()) { const ShopInfo::shop_item &item = *m_shop_item_it++; m_item = &(item.item); ASSERT(m_item->is_valid()); m_price = item.price; m_shop = &si; } } } ST_ItemIterator ST_ItemIterator::operator ++ (int dummy) { const ST_ItemIterator copy = *this; ++(*this); return (copy); }