/* NetHack 5.0	pickup.c	$NHDT-Date: 1773373633 2026/03/12 19:47:13 $  $NHDT-Branch: NetHack-3.7 $:$NHDT-Revision: 1.386 $ */
/* Copyright (c) Stichting Mathematisch Centrum, Amsterdam, 1985. */
/*-Copyright (c) Robert Patrick Rankin, 2012. */
/* NetHack may be freely redistributed.  See license for details. */

/*
 *      Contains code for picking objects up, and container use.
 */

#include "hack.h"

#define CONTAINED_SYM '>' /* from invent.c */

staticfn void simple_look(struct obj *, boolean);
staticfn boolean query_classes(char *, boolean *, boolean *, const char *,
                             struct obj *, boolean, int *);
staticfn boolean fatal_corpse_mistake(struct obj *, boolean);
staticfn boolean describe_decor(void);
staticfn void check_here(boolean);
staticfn boolean n_or_more(struct obj *);
staticfn boolean all_but_uchain(struct obj *);
#if 0 /* not used */
staticfn boolean allow_cat_no_uchain(struct obj *);
#endif
staticfn int autopick(struct obj *, int, menu_item **);
staticfn int count_categories(struct obj *, int);
staticfn int delta_cwt(struct obj *, struct obj *);
staticfn long carry_count(struct obj *, struct obj *, long, boolean, int *,
                        int *);
staticfn int lift_object(struct obj *, struct obj *, long *, boolean);
staticfn void pickup_prinv(struct obj *, long, const char *);
staticfn boolean mbag_explodes(struct obj *, int);
staticfn boolean is_boh_item_gone(void);
staticfn void do_boh_explosion(struct obj *, boolean);
staticfn long boh_loss(struct obj *, boolean);
staticfn int in_container(struct obj *);
staticfn int out_container(struct obj *);
staticfn long mbag_item_gone(boolean, struct obj *, boolean);
staticfn int stash_ok(struct obj *);
staticfn void explain_container_prompt(boolean);
staticfn int traditional_loot(boolean);
staticfn int menu_loot(int, boolean);
staticfn int tip_ok(struct obj *);
staticfn int choose_tip_container_menu(void);
staticfn struct obj *tipcontainer_gettarget(struct obj *, boolean *);
staticfn int tipcontainer_checks(struct obj *, struct obj *, boolean);
staticfn char in_or_out_menu(const char *, struct obj *, boolean, boolean,
                           boolean, boolean);
staticfn boolean able_to_loot(coordxy, coordxy, boolean);
staticfn boolean reverse_loot(void);
staticfn boolean mon_beside(coordxy, coordxy);
staticfn int do_loot_cont(struct obj **, int, int);
staticfn int doloot_core(void);
staticfn void tipcontainer(struct obj *);

/* define for query_objlist() and autopickup() */
#define FOLLOW(curr, flags) \
    (((flags) & BY_NEXTHERE) ? (curr)->nexthere : (curr)->nobj)

#define GOLD_WT(n) (((n) + 50L) / 100L)
/* if you can figure this out, give yourself a hearty pat on the back... */
#define GOLD_CAPACITY(w, n) (((w) * -100L) - ((n) + 50L) - 1L)

#define Icebox (gc.current_container->otyp == ICE_BOX)

static const char
    slightloadpfx[] = "You have a little trouble",
    moderateloadpfx[] = "You have trouble",
    nearloadpfx[] = "You have much trouble",
    overloadpfx[] = "You have extreme difficulty";

/* BUG: this lets you look at cockatrice corpses while blind without
   touching them */
/* much simpler version of the look-here code; used by query_classes() */
staticfn void
simple_look(struct obj *otmp, /* list of objects */
            boolean here)     /* flag for type of obj list linkage */
{
    /* Neither of the first two cases is expected to happen, since
     * we're only called after multiple classes of objects have been
     * detected, hence multiple objects must be present.
     */
    if (!otmp) {
        impossible("simple_look(null)");
    } else if (!(here ? otmp->nexthere : otmp->nobj)) {
        pline1(doname(otmp));
    } else {
        winid tmpwin = create_nhwindow(NHW_MENU);

        putstr(tmpwin, 0, "");
        do {
            putstr(tmpwin, 0, doname(otmp));
            otmp = here ? otmp->nexthere : otmp->nobj;
        } while (otmp);
        display_nhwindow(tmpwin, TRUE);
        destroy_nhwindow(tmpwin);
    }
}

int
collect_obj_classes(char ilets[], struct obj *otmp, boolean here,
                    boolean (*filter)(OBJ_P), int *itemcount)
{
    int iletct = 0;
    char c;

    *itemcount = 0;
    ilets[iletct] = '\0'; /* terminate ilets so that strchr() will work */
    while (otmp) {
        c = def_oc_syms[(int) otmp->oclass].sym;
        if (!strchr(ilets, c) && (!filter || (*filter)(otmp)))
            ilets[iletct++] = c, ilets[iletct] = '\0';
        *itemcount += 1;
        otmp = here ? otmp->nexthere : otmp->nobj;
    }

    return iletct;
}

/*
 * For menustyle:Traditional and menustyle:Combination.
 *
 * Suppose some '?' and '!' objects are present, but '/' objects aren't:
 *      "a" picks all items without further prompting;
 *      "A" steps through all items, asking one by one;
 *      "?" steps through '?' items, asking, and ignores '!' ones;
 *      "/" becomes 'A', since no '/' present;
 *      "?a" or "a?" picks all '?' without further prompting;
 *      "/a" or "a/" becomes 'A' since there aren't any '/'
 *          (bug fix:  3.1.0 thru 3.1.3 treated it as "a");
 *      "?/a" or "a?/" or "/a?",&c picks all '?' even though no '/'
 *          (ie, treated as if it had just been "?a").
 *
 * Note: the behavior and meaning of 'a' vs 'A' is effectively reversed
 * when using menustyle:Full.  For Traditional, the choice is based on
 * ease of typing (using 'a' is much more common than 'A'); for Full,
 * it was changed to enhance menu entry ordering ('A' stands out, but
 * some players complain that it is too easy to choose accidentally).
 */
staticfn boolean
query_classes(
    char oclasses[], /* selected classes */
    boolean *one_at_a_time, /* to tell caller that user picked 'A' */
    boolean *everything, /* to tell caller that user picked 'a' */
    const char *action, /* verb for what activity needs objects */
    struct obj *objs, /* invent or container->cobj or level.objects[x][y] */
    boolean here, /* True: traverse by obj->nexthere; False: by obj->nobj */
    int *menu_on_demand) /* to tell caller that user picked 'm' */
{
    char ilets[36], inbuf[BUFSZ] = DUMMY; /* FIXME: hardcoded ilets[] length */
    int iletct, oclassct;
    boolean not_everything, filtered;
    char qbuf[QBUFSZ];
    boolean m_seen;
    int itemcount, bcnt, ucnt, ccnt, xcnt, ocnt, jcnt;

    oclasses[oclassct = 0] = '\0';
    *one_at_a_time = *everything = m_seen = FALSE;
    if (menu_on_demand)
        *menu_on_demand = 0;
    iletct = collect_obj_classes(ilets, objs, here,
                                 (boolean (*)(OBJ_P)) 0, &itemcount);
    if (iletct == 0)
        return FALSE;

    if (iletct == 1) {
        oclasses[0] = def_char_to_objclass(ilets[0]);
        oclasses[1] = '\0';
    } else { /* more than one choice available */
        /* additional choices */
        ilets[iletct++] = ' ';
        ilets[iletct++] = 'a';
        ilets[iletct++] = 'A';
        ilets[iletct++] = (objs == gi.invent ? 'i' : ':');
    }
    if (itemcount && menu_on_demand)
        ilets[iletct++] = 'm';
    if (count_unpaid(objs))
        ilets[iletct++] = 'u';

    tally_BUCX(objs, here, &bcnt, &ucnt, &ccnt, &xcnt, &ocnt, &jcnt);
    if (bcnt)
        ilets[iletct++] = 'B';
    if (ucnt)
        ilets[iletct++] = 'U';
    if (ccnt)
        ilets[iletct++] = 'C';
    if (xcnt)
        ilets[iletct++] = 'X';
    if (jcnt)
        ilets[iletct++] = 'P';
    ilets[iletct] = '\0';

    if (iletct > 1) {
        const char *where = 0;
        char sym, oc_of_sym, *p;

 ask_again:
        oclasses[oclassct = 0] = '\0';
        *one_at_a_time = *everything = FALSE;
        not_everything = filtered = FALSE;
        Sprintf(qbuf, "What kinds of thing do you want to %s? [%s]", action,
                ilets);
        getlin(qbuf, inbuf);
        if (*inbuf == '\033')
            return FALSE;

        for (p = inbuf; (sym = *p++) != 0; ) {
            if (sym == ' ')
                continue;
            else if (sym == 'A')
                *one_at_a_time = TRUE;
            else if (sym == 'a')
                *everything = TRUE;
            else if (sym == ':') {
                simple_look(objs, here); /* dumb if objs==invent */
                /* if we just scanned the contents of a container
                   then mark it as having known contents */
                if (objs->where == OBJ_CONTAINED)
                    objs->ocontainer->cknown = 1;
                goto ask_again;
            } else if (sym == 'i') {
                (void) display_inventory((char *) 0, TRUE);
                goto ask_again;
            } else if (sym == 'm') {
                m_seen = TRUE;
            } else if (strchr("uBUCXP", sym)) {
                add_valid_menu_class(sym); /* 'u' or 'B','U','C','X','P' */
                filtered = TRUE;
            } else {
                oc_of_sym = def_char_to_objclass(sym);
                if (strchr(ilets, sym)) {
                    add_valid_menu_class(oc_of_sym);
                    oclasses[oclassct++] = oc_of_sym;
                    oclasses[oclassct] = '\0';
                } else {
                    if (!where)
                        where = !strcmp(action, "pick up") ? "here"
                                : !strcmp(action, "take out") ? "inside" : "";
                    if (*where)
                        There("are no %c's %s.", sym, where);
                    else
                        You("have no %c's.", sym);
                    not_everything = TRUE;
                }
            }
        } /* for p:sym in inbuf */

        if (m_seen && menu_on_demand) {
            *menu_on_demand = (((*everything || !oclassct) && !filtered)
                               ? -2 : -3);
            return FALSE;
        }
        if (!oclassct && (!*everything || not_everything)) {
            /* didn't pick anything,
               or tried to pick something that's not present */
            *one_at_a_time = TRUE; /* force 'A' */
            *everything = FALSE;   /* inhibit 'a' */
        }
    }
    return TRUE;
}

/*
 * tests:
 *  st_gloves      wearing gloves?
 *  st_corpse      is it a corpse obj?
 *  st_petrifies   does the corpse petrify on touch?
 *  st_resists     does hero have stoning resistance?
 *  st_all         st_gloves | st_corpse | st_petrifies | st_resists
 */
boolean
u_safe_from_fatal_corpse(struct obj *obj, int tests)
{
    if (((tests & st_gloves) && uarmg)
        || ((tests & st_corpse) && obj->otyp != CORPSE)
        || ((tests & st_petrifies) && !touch_petrifies(&mons[obj->corpsenm]))
        || ((tests & st_resists) && Stone_resistance))
        return TRUE;
    return FALSE;
}

/* check whether hero is bare-handedly touching a cockatrice corpse */
staticfn boolean
fatal_corpse_mistake(struct obj *obj, boolean remotely)
{
    if (u_safe_from_fatal_corpse(obj, st_all) || remotely)
        return FALSE;

    if (poly_when_stoned(gy.youmonst.data) && polymon(PM_STONE_GOLEM)) {
        display_nhwindow(WIN_MESSAGE, FALSE); /* --More-- */
        return FALSE;
    }

    pline("Touching %s is a fatal mistake.",
          corpse_xname(obj, (const char *) 0, CXN_SINGULAR | CXN_ARTICLE));
    instapetrify(killer_xname(obj));
    return TRUE;
}

/* attempting to manipulate a Rider's corpse triggers its revival */
boolean
rider_corpse_revival(struct obj *obj, boolean remotely)
{
    if (!obj || obj->otyp != CORPSE || !is_rider(&mons[obj->corpsenm]))
        return FALSE;

    pline("At your %s, the corpse suddenly moves...",
          remotely ? "attempted acquisition" : "touch");
    (void) revive_corpse(obj);
    exercise(A_WIS, FALSE);
    return TRUE;
}

/* wand of probing zapped down; perhaps hero is levitating while blind */
void
force_decor(boolean via_probing)
{
    /* we don't want describe_decor() to defer feedback if hero is fumbling
       with 1 turn left until next slip_or_trip(), or for ice_descr() to
       omit thawing details if hero is probing when levitating while blind
       (those will be skipped for look_here() and farlook() or autodescribe);
       we can't control that by temporarily tweaking properties because that
       could become noticeable if status gets updated while decor feedback
       is being delivered */
    gd.decor_fumble_override = TRUE;
    gd.decor_levitate_override = via_probing;
    /* force current terrain to be different from previous location, or
       uninteresting if previous location was actually inside solid stone */
    iflags.prev_decor = STONE;
    (void) describe_decor();
    gd.decor_fumble_override = gd.decor_levitate_override = FALSE;
    svl.lastseentyp[u.ux][u.uy] = levl[u.ux][u.uy].typ;
}

void
deferred_decor(
    boolean setup) /* True: deferring, False: catching up */
{
    if (!flags.mention_decor) {
        iflags.defer_decor = FALSE;
    } else if (setup) {
        iflags.defer_decor = TRUE;
    } else {
        (void) describe_decor();
        iflags.defer_decor = FALSE;
    }
}

/* handle 'mention_decor' (when walking onto a dungeon feature such as
   stairs or altar, describe it even if it isn't covered up by an object) */
staticfn boolean
describe_decor(void)
{
    char outbuf[BUFSZ], fbuf[QBUFSZ];
    boolean doorhere, waterhere, res = TRUE;
    const char *dfeature;
    int ltyp;

    if ((HFumbling & TIMEOUT) == 1L /* about to slip_or_trip */
        && !iflags.defer_decor
        && !gd.decor_fumble_override) { /* probe_decor() */
        /*
         * Work around a message sequencing issue if Fumbling's periodic
         * timeout is about to kick in:  avoid the combination
         *  |You are back on floor.
         *  |You trip over <object>.  or  You flounder.
         * when the trip is being caused by moving on ice as hero
         * steps off ice onto non-ice.  Defer the back-on-floor part if
         * that is about to happen.
         */
        deferred_decor(TRUE);
        return FALSE;
    }

    ltyp = SURFACE_AT(u.ux, u.uy);
    dfeature = dfeature_at(u.ux, u.uy, fbuf);

    /* we don't mention "ordinary" doors but do mention broken ones (and
       closed ones, which will only happen for Passes_walls) */
    doorhere = dfeature && (!strcmp(dfeature, "open door")
                            || !strcmp(dfeature, "doorway"));
    waterhere = dfeature && !strcmp(dfeature, "pool of water");
    if (doorhere || Underwater
        || (ltyp == ICE && IS_POOL(iflags.prev_decor))) /* pooleffects() */
        dfeature = 0;
    /*
     * TODO: if on ice, report moving between thicker and thinner ice (based
     * on ice_descr()'s classification) as if moving onto different terrain.
     */

    if (ltyp == iflags.prev_decor && !IS_FURNITURE(ltyp)) {
        res = FALSE;
    } else if (dfeature) {
        if (waterhere)
            dfeature = strcpy(fbuf, waterbody_name(u.ux, u.uy));
        if (strcmp(dfeature, "swamp") && ltyp != ICE)
            dfeature = an(dfeature);

        if (flags.verbose) {
            Sprintf(outbuf, "There is %s here.", dfeature);
        } else {
            if (dfeature != fbuf)
                Strcpy(fbuf, dfeature);
            Sprintf(outbuf, "%s.", upstart(fbuf));
        }
        if (ltyp == ICE && flags.mention_decor)
            Norep("%s", outbuf);
        else
            pline("%s", outbuf);
    } else if (!Underwater) {
        if (IS_POOL(iflags.prev_decor)
            || IS_LAVA(iflags.prev_decor)
            || iflags.prev_decor == ICE) {
            if (iflags.last_msg != PLNMSG_BACK_ON_GROUND) {
                back_on_ground(FALSE);
            }
        }
    }
    /* describe_decor() is normally called when moving onto a different
       type of terrain, but it is also called by pickup() even when
       mention_decor is Off if hero can't reach floor; only adapt the next
       describe_decor() by what has just occurred in this one when it's On */
    iflags.prev_decor = flags.mention_decor ? ltyp : STONE;
    return res;
}

/* look at the objects at our location, unless there are too many of them */
staticfn void
check_here(boolean picked_some)
{
    struct obj *obj;
    int ct = 0;
    unsigned lhflags = picked_some ? LOOKHERE_PICKED_SOME : LOOKHERE_NOFLAGS;

    if (flags.mention_decor) {
        if (describe_decor())
            lhflags |= LOOKHERE_SKIP_DFEATURE;
    }

    /* count the objects here */
    for (obj = svl.level.objects[u.ux][u.uy]; obj; obj = obj->nexthere) {
        if (obj != uchain)
            ct++;
    }

    /* If there are objects here, take a look. */
    if (ct) {
        if (svc.context.run)
            nomul(0);
        flush_screen(1);
        (void) look_here(ct, lhflags);
    } else {
        read_engr_at(u.ux, u.uy);
    }
}

/* query_objlist callback: return TRUE if obj's count is >= reference value */
staticfn boolean
n_or_more(struct obj *obj)
{
    if (obj == uchain)
        return FALSE;
    return (boolean) (obj->quan >= gv.val_for_n_or_more);
}

/* check valid_menu_classes[] for an entry; also used by askchain() */
boolean
menu_class_present(int c)
{
    return (c && strchr(gv.valid_menu_classes, c)) ? TRUE : FALSE;
}

void
add_valid_menu_class(int c)
{
    static int vmc_count = 0;

    if (c == 0) { /* reset */
        vmc_count = 0;
        gc.class_filter = gb.bucx_filter = gs.shop_filter = FALSE;
        gp.picked_filter = FALSE;
    } else if (!menu_class_present(c)) {
        gv.valid_menu_classes[vmc_count++] = (char) c;
        /* categorize the new class */
        switch (c) {
        case 'B':
        case 'U':
        case 'C': /*FALLTHRU*/
        case 'X':
            gb.bucx_filter = TRUE;
            break;
        case 'P':
            gp.picked_filter = TRUE;
            break;
        case 'u':
            gs.shop_filter = TRUE;
            break;
        default:
            gc.class_filter = TRUE;
            break;
        }
    }
    gv.valid_menu_classes[vmc_count] = '\0';
}

/* query_objlist callback: return TRUE if not uchain */
staticfn boolean
all_but_uchain(struct obj *obj)
{
    return (boolean) (obj != uchain);
}

/* query_objlist callback: return TRUE */
/*ARGUSED*/
boolean
allow_all(struct obj *obj UNUSED)
{
    return TRUE;
}

boolean
allow_category(struct obj *obj)
{
    /* If no filters are active, nothing will match unless
       paranoid_confirm:A is set. */
    if (!gc.class_filter && !gs.shop_filter && !gb.bucx_filter
        && !gp.picked_filter && !ParanoidAutoAll)
        return FALSE;

    /* For coins, if any class filter is specified, accept if coins
     * are included regardless of whether either unpaid or BUC-status
     * is also specified since player has explicitly requested coins.
     */
    if (obj->oclass == COIN_CLASS && gc.class_filter)
        return strchr(gv.valid_menu_classes, COIN_CLASS) ? TRUE : FALSE;

    if (Role_if(PM_CLERIC) && !obj->bknown)
        set_bknown(obj, 1);

    /*
     * Version 3.6 had three types of filters possible and the first
     * and third can have more than one entry:
     *  1) object class (armor, potion, &c);
     *  2) unpaid shop item;
     *  3) bless/curse state (blessed, uncursed, cursed, BUC-unknown).
     * Version 5.0 added a fourth:
     *  4) 'novelty' ('P' for just picked up items).
     * When only one type is present, the situation is simple:
     * to be accepted, obj's status must match one of the entries.
     * When more than one type is present, the obj will now only
     * be accepted when it matches one entry of each type.
     * So ?!B will accept blessed scrolls or potions, and [u will
     * accept unpaid armor.  (In 3.4.3, an object was accepted by
     * this filter if it met any entry of any type, so ?!B resulted
     * in accepting all scrolls and potions regardless of bless/curse
     * state plus all blessed non-scroll, non-potion objects.)
     */

    /* if class is expected but obj's class is not in the list, reject */
    if (gc.class_filter && !strchr(gv.valid_menu_classes, obj->oclass))
        return FALSE;
    /* if unpaid is expected and obj isn't unpaid, reject (treat a container
       holding any unpaid object as unpaid even if isn't unpaid itself) */
    if (gs.shop_filter && !obj->unpaid
        && !(Has_contents(obj) && count_unpaid(obj->cobj) > 0))
        return FALSE;
    /* check for particular bless/curse state */
    if (gb.bucx_filter) {
        /* first categorize this object's bless/curse state */
        char bucx;
        if (obj->oclass == COIN_CLASS) {
            /* If no class filtering is specified but bless/curse state is,
               coins are treated as either unknown or uncursed based on an
               option setting. */
            bucx = flags.goldX ? 'X' : 'U';
        } else {
            bucx = !obj->bknown ? 'X'
                     : obj->blessed ? 'B'
                        : obj->cursed ? 'C'
                           : 'U';
        }

        /* if its category is not in the list, reject */
        if (!strchr(gv.valid_menu_classes, bucx))
            return FALSE;
    }
    if (gp.picked_filter && !obj->pickup_prev)
        return FALSE;
    /* obj didn't fail any of the filter checks, so accept */
    return TRUE;
}

#if 0 /* not used */
/* query_objlist callback: return TRUE if valid category (class), no uchain */
staticfn boolean
allow_cat_no_uchain(struct obj *obj)
{
    if (obj != uchain
        && ((strchr(gv.valid_menu_classes, 'u') && obj->unpaid)
            || strchr(gv.valid_menu_classes, obj->oclass)))
        return TRUE;
    return FALSE;
}
#endif

/* query_objlist callback: return TRUE if valid class and worn */
boolean
is_worn_by_type(struct obj *otmp)
{
    return (is_worn(otmp) && allow_category(otmp)) ? TRUE : FALSE;
}

/* reset last-picked-up flags */
void
reset_justpicked(struct obj *olist)
{
    struct obj *otmp;
    /*
     * TODO?  Possible enhancement: don't reset if hero is still at same
     *  spot where most recent pickup took place.  Not resetting will be
     *  the correct behavior for autopickup immediately followed by manual
     *  pickup.  It would probably be correct for either or both pickups
     *  followed by manual pickup of a newly arrived missile after some
     *  time has elapsed.  Things becomes murkier for other activity.
     *  Taking anything out of a container ought to be treated as if
     *  having moved to another spot.
     */

    for (otmp = olist; otmp; otmp = otmp->nobj)
        otmp->pickup_prev = 0;
}

int
count_justpicked(struct obj *olist)
{
    struct obj *otmp;
    int cnt = 0;

    for (otmp = olist; otmp; otmp = otmp->nobj)
        if (otmp->pickup_prev)
            cnt++;

    return cnt;
}

struct obj *
find_justpicked(struct obj *olist)
{
    struct obj *otmp;

    for (otmp = olist; otmp; otmp = otmp->nobj)
        if (otmp->pickup_prev)
            return otmp;

    return (struct obj *) 0;
}

/*
 * Have the hero pick things from the ground
 * or a monster's inventory if swallowed.
 *
 * Arg what:
 *      >0  autopickup
 *      =0  interactive
 *      <0  pickup count of something
 *
 * Returns 1 if tried to pick something up, whether
 * or not it succeeded.
 */
int
pickup(int what) /* should be a long */
{
    int i, n, res, count, n_tried = 0, n_picked = 0;
    menu_item *pick_list = (menu_item *) 0;
    boolean autopickup = what > 0;
    struct obj **objchain_p;
    int traverse_how;

    /* we might have arrived here while fainted or sleeping, via
       random teleport or levitation timeout; if so, skip check_here
       and read_engr_at in addition to bypassing autopickup itself
       [probably ought to check whether hero is using a cockatrice
       corpse for a pillow here... (also at initial faint/sleep)] */
    if (autopickup && gm.multi < 0 && unconscious()) {
        iflags.prev_decor = STONE;
        return 0;
    }

    /* used by pickup_object() for encumbrance feedback */
    gp.pickup_encumbrance = 0;

    if (what < 0) /* pick N of something */
        count = -what;
    else /* pick anything */
        count = 0;

    if (!u.uswallow) {
        struct trap *t;

        /* no auto-pick if no-pick move, nothing there, or in a pool */
        if (autopickup && (svc.context.nopick || !OBJ_AT(u.ux, u.uy)
                           || (is_pool(u.ux, u.uy) && !Underwater)
                           || is_lava(u.ux, u.uy))) {
            if (flags.mention_decor)
                (void) describe_decor();
            read_engr_at(u.ux, u.uy);
            return 0;
        }
        /* no pickup if levitating & not on air or water level */
        t = t_at(u.ux, u.uy);
        if (!can_reach_floor(t && is_pit(t->ttyp))) {
            (void) describe_decor(); /* even when !flags.mention_decor */
            if ((gm.multi && !svc.context.run)
                || (autopickup && !flags.pickup)
                || (t && (uteetering_at_seen_pit(t) || uescaped_shaft(t))))
                read_engr_at(u.ux, u.uy);
            return 0;
        }
        /* multi && !svc.context.run means they are in the middle of some
         * other action, or possibly paralyzed, sleeping, etc.... and they
         * just teleported onto the object.  They shouldn't pick it up.
         */
        if ((gm.multi && !svc.context.run)
            || (autopickup && !flags.pickup)
            || notake(gy.youmonst.data)) {
            check_here(FALSE);
            if (notake(gy.youmonst.data) && OBJ_AT(u.ux, u.uy)
                && (autopickup || flags.pickup))
                You("are physically incapable of picking anything up.");
            return 0;
        }

        /* if there's anything here, stop running */
        if (OBJ_AT(u.ux, u.uy) && svc.context.run && svc.context.run != 8
            && !svc.context.nopick)
            nomul(0);
    }

    add_valid_menu_class(0); /* reset */
    if (!u.uswallow) {
        objchain_p = &svl.level.objects[u.ux][u.uy];
        traverse_how = BY_NEXTHERE;
    } else {
        objchain_p = &u.ustuck->minvent;
        traverse_how = 0; /* nobj */
    }
    /*
     * Start the actual pickup process.  This is split into two main
     * sections, the newer menu and the older "traditional" methods.
     * Automatic pickup has been split into its own menu-style routine
     * to make things less confusing.
     */
    if (autopickup) {
        n = autopick(*objchain_p, traverse_how, &pick_list);
        goto menu_pickup;
    }

    if (flags.menu_style != MENU_TRADITIONAL || iflags.menu_requested) {
        /* use menus exclusively */
        traverse_how |= AUTOSELECT_SINGLE
                        | (flags.sortpack ? INVORDER_SORT : 0);
        if (count) { /* looking for N of something */
            char qbuf[QBUFSZ];

            Sprintf(qbuf, "Pick %d of what?", count);
            gv.val_for_n_or_more = count; /* set up callback selector */
            n = query_objlist(qbuf, objchain_p, traverse_how,
                              &pick_list, PICK_ONE, n_or_more);
            /* correct counts, if any given */
            for (i = 0; i < n; i++)
                pick_list[i].count = count;
        } else {
            n = query_objlist("Pick up what?", objchain_p,
                              (traverse_how | FEEL_COCKATRICE),
                              &pick_list, PICK_ANY, all_but_uchain);
        }

 menu_pickup:
        if (n > 0)
            reset_justpicked(gi.invent);
        n_tried = n;
        for (n_picked = i = 0; i < n; i++) {
            res = pickup_object(pick_list[i].item.a_obj, pick_list[i].count,
                                FALSE);
            if (res < 0)
                break; /* can't continue */
            n_picked += res;
        }
        if (pick_list)
            free((genericptr_t) pick_list);

    } else {
        /* old style interface */
        int ct = 0;
        long lcount;
        boolean all_of_a_type, selective, bycat;
        char oclasses[MAXOCLASSES + 10]; /* +10: room for B,U,C,X plus slop */
        struct obj *obj, *obj2;

        oclasses[0] = '\0';   /* types to consider (empty for all) */
        all_of_a_type = TRUE; /* take all of considered types */
        selective = FALSE;    /* ask for each item */

        /* check for more than one object */
        for (obj = *objchain_p; obj; obj = FOLLOW(obj, traverse_how))
            ct++;

        if (ct == 1 && count) {
            /* if only one thing, then pick it */
            obj = *objchain_p;
            lcount = min(obj->quan, (long) count);
            n_tried++;
            reset_justpicked(gi.invent);
            if (pickup_object(obj, lcount, FALSE) > 0)
                n_picked++; /* picked something */
            goto end_query;

        } else if (ct >= 2) {
            int via_menu = 0;

            There("are %s objects here.", (ct <= 10) ? "several" : "many");
            if (!query_classes(oclasses, &selective, &all_of_a_type,
                               "pick up", *objchain_p,
                               (traverse_how & BY_NEXTHERE) ? TRUE : FALSE,
                               &via_menu)) {
                if (!via_menu)
                    goto pickupdone;
                if (selective)
                    traverse_how |= INVORDER_SORT;
                n = query_objlist("Pick up what?", objchain_p, traverse_how,
                                  &pick_list, PICK_ANY,
                                  (via_menu == -2) ? allow_all
                                                   : allow_category);
                goto menu_pickup;
            }
        }
        bycat = (menu_class_present('B') || menu_class_present('U')
                 || menu_class_present('C') || menu_class_present('X'));

        for (obj = *objchain_p; obj; obj = obj2) {
            obj2 = FOLLOW(obj, traverse_how);
            if (bycat ? !allow_category(obj)
                      : (!selective && oclasses[0]
                         && !strchr(oclasses, obj->oclass)))
                continue;

            lcount = -1L;
            if (!all_of_a_type) {
                char qbuf[BUFSZ];

                (void) safe_qbuf(qbuf, "Pick up ", "?", obj, doname,
                                 ansimpleoname, something);
                switch ((obj->quan < 2L) ? ynaq(qbuf) : ynNaq(qbuf)) {
                case 'q':
                    goto end_query; /* out 2 levels */
                case 'n':
                    continue;
                case 'a':
                    all_of_a_type = TRUE;
                    if (selective) {
                        selective = FALSE;
                        oclasses[0] = obj->oclass;
                        oclasses[1] = '\0';
                    }
                    break;
                case '#': /* count was entered */
                    if (!yn_number)
                        continue; /* 0 count => No */
                    lcount = (long) yn_number;
                    if (lcount > obj->quan)
                        lcount = obj->quan;
                    FALLTHROUGH;
                    /*FALLTHRU*/
                default: /* 'y' */
                    break;
                }
            }
            if (lcount == -1L)
                lcount = obj->quan;

            if (!n_tried) /* reset just before the first item picked */
                reset_justpicked(gi.invent);

            n_tried++;
            if ((res = pickup_object(obj, lcount, FALSE)) < 0)
                break;
            n_picked += res;
        }
 end_query:
        ; /* statement required after label */
    }

    if (!u.uswallow) {
        if (hides_under(gy.youmonst.data))
            (void) hideunder(&gy.youmonst);

        /* position may need updating (invisible hero) */
        if (n_picked)
            newsym_force(u.ux, u.uy);

        /* check if there's anything else here after auto-pickup is done */
        if (autopickup)
            check_here(n_picked > 0);
    }
 pickupdone:
    gp.pickup_encumbrance = 0;
    add_valid_menu_class(0); /* reset */
    return (n_tried > 0);
}

struct autopickup_exception *
check_autopickup_exceptions(struct obj *obj)
{
    /*
     *  Does the text description of this match an exception?
     */
    struct autopickup_exception *ape = ga.apelist;

    if (ape) {
        char *objdesc = makesingular(doname(obj));

        while (ape && !regex_match(objdesc, ape->regex))
            ape = ape->next;
    }
    return ape;
}

boolean
autopick_testobj(struct obj *otmp, boolean calc_costly)
{
    struct autopickup_exception *ape;
    static boolean costly = FALSE;
    const char *otypes = flags.pickup_types;
    boolean pickit;

    /* calculate 'costly' just once for a given autopickup operation */
    if (calc_costly)
        costly = (otmp->where == OBJ_FLOOR
                  && costly_spot(otmp->ox, otmp->oy));

    /* first check: reject if an unpaid item in a shop */
    if (costly && !otmp->no_charge)
        return FALSE;

    /* pickup_thrown/pickup_stolen/nopick_dropped override pickup_types and
       exceptions */
    if ((flags.pickup_thrown && otmp->how_lost == LOST_THROWN)
        || (flags.pickup_stolen && otmp->how_lost == LOST_STOLEN))
        return TRUE;
    if (flags.nopick_dropped && otmp->how_lost == LOST_DROPPED)
        return FALSE;
    if (otmp->how_lost == LOST_EXPLODING)
        return FALSE;

    /* check for pickup_types */
    pickit = (!*otypes || strchr(otypes, otmp->oclass));

    /* check for autopickup exceptions */
    ape = check_autopickup_exceptions(otmp);
    if (ape)
        pickit = ape->grab;

    return pickit;
}

/*
 * Pick from the given list using flags.pickup_types.  Return the number
 * of items picked (not counts).  Create an array that returns pointers
 * and counts of the items to be picked up.  If the number of items
 * picked is zero, the pickup list is left alone.  The caller of this
 * function must free the pickup list.
 */
staticfn int
autopick(
    struct obj *olist,     /* the object list */
    int follow,            /* how to follow the object list */
    menu_item **pick_list) /* list of objects and counts to pick up */
{
    menu_item *pi; /* pick item */
    struct obj *curr;
    int n;
    boolean check_costly = TRUE;

    /* first count the number of eligible items */
    for (n = 0, curr = olist; curr; curr = FOLLOW(curr, follow)) {
        if (autopick_testobj(curr, check_costly))
            ++n;
        check_costly = FALSE; /* only need to check once per autopickup */
    }

    if (n) {
        *pick_list = pi = (menu_item *) alloc(sizeof (menu_item) * n);
        for (n = 0, curr = olist; curr; curr = FOLLOW(curr, follow)) {
            if (autopick_testobj(curr, FALSE)) {
                pi[n].item.a_obj = curr;
                pi[n].count = curr->quan;
                n++;
            }
        }
    }
    return n;
}

/*
 * Put up a menu using the given object list.  Only those objects on the
 * list that meet the approval of the allow function are displayed.  Return
 * a count of the number of items selected, as well as an allocated array of
 * menu_items, containing pointers to the objects selected and counts.  The
 * returned counts are guaranteed to be in bounds and non-zero.
 *
 * Query flags:
 *      BY_NEXTHERE       - Follow object list via nexthere instead of nobj.
 *      AUTOSELECT_SINGLE - Don't ask if only 1 object qualifies - just
 *                          use it.
 *      USE_INVLET        - Use object's invlet.
 *      INVORDER_SORT     - Use hero's pack order.
 *      INCLUDE_HERO      - Showing engulfer's invent; show hero too.
 *      SIGNAL_NOMENU     - Return -1 rather than 0 if nothing passes "allow".
 *      SIGNAL_ESCAPE     - Return -1 rather than 0 if player uses ESC to
 *                          pick nothing.
 *      FEEL_COCKATRICE   - touch corpse.
 */
int
query_objlist(const char *qstr,        /* query string */
              struct obj **olist_p,    /* the list to pick from */
              int qflags,              /* options to control the query */
              menu_item **pick_list,   /* return list of items picked */
              int how,                 /* type of query */
              boolean (*allow)(OBJ_P)) /* allow function */
{
    int i, n, tmpglyph;
    winid win;
    struct obj *curr, *last, fake_hero_object, *olist = *olist_p;
    char *pack, packbuf[MAXOCLASSES + 1];
    anything any;
    boolean printed_type_name, first,
            sorted = (qflags & INVORDER_SORT) != 0,
            engulfer = (qflags & INCLUDE_HERO) != 0,
            engulfer_minvent;
    unsigned sortflags;
    glyph_info tmpglyphinfo = nul_glyphinfo;
    Loot *sortedolist, *srtoli;
    int clr = NO_COLOR;

    *pick_list = (menu_item *) 0;
    if (!olist && !engulfer)
        return 0;

    /* count the number of items allowed */
    for (n = 0, last = 0, curr = olist; curr; curr = FOLLOW(curr, qflags))
        if ((*allow)(curr)) {
            last = curr;
            n++;
        }
    /* can't depend upon 'engulfer' because that's used to indicate whether
       hero should be shown as an extra, fake item */
    engulfer_minvent = (olist && olist->where == OBJ_MINVENT
                        && engulfing_u(olist->ocarry));
    if (engulfer_minvent && n == 1 && olist->owornmask != 0L) {
        qflags &= ~AUTOSELECT_SINGLE;
    }
    if (engulfer) {
        ++n;
        /* don't autoselect swallowed hero if it's the only choice */
        qflags &= ~AUTOSELECT_SINGLE;
    }

    if (n == 0) /* nothing to pick here */
        return (qflags & SIGNAL_NOMENU) ? -1 : 0;

    if (n == 1 && (qflags & AUTOSELECT_SINGLE)) {
        *pick_list = (menu_item *) alloc(sizeof (menu_item));
        (*pick_list)->item.a_obj = last;
        (*pick_list)->count = last->quan;
        return 1;
    }

    sortflags = (((flags.sortloot == 'f'
                   || (flags.sortloot == 'l' && !(qflags & USE_INVLET)))
                  ? SORTLOOT_LOOT
                  : ((qflags & USE_INVLET) ? SORTLOOT_INVLET : 0))
                 | (flags.sortpack ? SORTLOOT_PACK : 0)
                 | ((qflags & FEEL_COCKATRICE) ? SORTLOOT_PETRIFY : 0));
    sortedolist = sortloot(&olist, sortflags,
                           (qflags & BY_NEXTHERE) ? TRUE : FALSE, allow);

    win = create_nhwindow(NHW_MENU);
    start_menu(win, MENU_BEHAVE_STANDARD);
    any = cg.zeroany;
    if (gt.this_title) {
        /* dotypeinv() supplies gt.this_title to display as initial header;
           intentionally avoid the menu_headings highlight attribute here */
        add_menu_str(win, gt.this_title);
    }
    /*
     * Run through the list and add the objects to the menu.  If
     * INVORDER_SORT is set, we'll run through the list once for
     * each type so we can group them.  The allow function was
     * called by sortloot() and will be called once per item here.
     */
    pack = strcpy(packbuf, flags.inv_order);
    if (qflags & INCLUDE_VENOM)
        (void) strkitten(pack, VENOM_CLASS); /* venom is not in inv_order */
    first = TRUE;
    do {
        printed_type_name = FALSE;
        for (srtoli = sortedolist; ((curr = srtoli->obj) != 0); ++srtoli) {
            if (sorted && curr->oclass != *pack)
                continue;
            if ((qflags & FEEL_COCKATRICE) && curr->otyp == CORPSE
                && will_feel_cockatrice(curr, FALSE)) {
                destroy_nhwindow(win); /* stop the menu and revert */
                (void) look_here(0, LOOKHERE_NOFLAGS);
                unsortloot(&sortedolist);
                return 0;
            }
            if ((*allow)(curr)) {
                /* if sorting, print type name (once only) */
                if (sorted && !printed_type_name) {
                    boolean with_oc_sym = (how != PICK_NONE
                                           && iflags.menu_head_objsym);

                    any = cg.zeroany;
                    add_menu_heading(win,
                                     let_to_name(*pack, FALSE, with_oc_sym));
                    printed_type_name = TRUE;
                }

                any.a_obj = curr;
                tmpglyph = obj_to_glyph(curr, rn2_on_display_rng);
                map_glyphinfo(0, 0, tmpglyph, 0U, &tmpglyphinfo);
                add_menu(win, &tmpglyphinfo, &any,
                         (qflags & USE_INVLET) ? curr->invlet
                           : (first && curr->oclass == COIN_CLASS) ? '$' : 0,
                         def_oc_syms[(int) objects[curr->otyp].oc_class].sym,
                         ATR_NONE, clr, doname_with_price(curr),
                         MENU_ITEMFLAGS_NONE);
                first = FALSE;
            }
        }
        pack++;
    } while (sorted && *pack);
    unsortloot(&sortedolist);

    if (engulfer) {
        char buf[BUFSZ];

        any = cg.zeroany;
        if (sorted && n > 1) {
            Sprintf(buf, "%s Creatures",
                    digests(u.ustuck->data) ? "Swallowed" : "Engulfed");
            add_menu_heading(win, buf);
        }
        fake_hero_object = cg.zeroobj;
        fake_hero_object.quan = 1L; /* not strictly necessary... */
        any.a_obj = &fake_hero_object;
        tmpglyph = mon_to_glyph(&gy.youmonst, rn2_on_display_rng);
        map_glyphinfo(0, 0, tmpglyph, 0U, &tmpglyphinfo);
        add_menu(win, &tmpglyphinfo, &any,
                 /* fake inventory letter, no group accelerator */
                 CONTAINED_SYM, 0, ATR_NONE, clr, an(self_lookat(buf)),
                 MENU_ITEMFLAGS_NONE);
    }

    end_menu(win, qstr);
    n = select_menu(win, how, pick_list);
    destroy_nhwindow(win);

    if (n > 0) {
        menu_item *mi;
        int k;

        /* fix up counts:  -1 means no count used => pick all;
           if fake_hero_object was picked, discard that choice */
        for (i = k = 0, mi = *pick_list; i < n; i++, mi++) {
            curr = mi->item.a_obj;
            if (curr == &fake_hero_object) {
                /* this isn't actually possible; fake item representing
                   hero is only included for look here (':'), not pickup,
                   and that's PICK_NONE so we can't get here from there */
                You_cant("pick yourself up!");
                continue;
            }
            if (engulfer_minvent && curr->owornmask != 0L) {
                You_cant("pick %s up.", ysimple_name(curr));
                continue;
            }
            if (mi->count == -1L || mi->count > curr->quan)
                mi->count = curr->quan;
            if (k < i)
                (*pick_list)[k] = *mi;
            ++k;
        }
        if (!k) {
            /* fake_hero was only choice so discard whole list */
            free((genericptr_t) *pick_list);
            *pick_list = 0;
            n = 0;
        } else if (k < n) {
            /* other stuff plus fake_hero; last slot is now unused
               (could be more than one if player tried to pick items
               worn by engulfer) */
            while (n > k) {
                --n;
                (*pick_list)[n].item = cg.zeroany;
                (*pick_list)[n].count = 0L;
            }
        }
    } else if (n < 0) {
        /* -1 is used for SIGNAL_NOMENU, so callers don't expect it
           to indicate that the player declined to make a choice */
        n = (qflags & SIGNAL_ESCAPE) ? -2 : 0;
    }
    return n;
}

/*
 * For menustyle:Full.
 *
 * allow menu-based category (class) selection (for Drop,take off etc.)
 *
 * If ParanoidAutoAll, requires confirmation when 'A' has been picked.
 */
int
query_category(
    const char *qstr,      /* query string */
    struct obj *olist,     /* the list to pick from */
    int qflags,            /* behavior modification flags */
    menu_item **pick_list, /* return list of items picked */
    int how)               /* type of query */
{
    int n;
    winid win;
    struct obj *curr;
    char *pack, packbuf[MAXOCLASSES + 1];
    anything any;
    boolean collected_type_name;
    char invlet;
    int ccount;
    boolean (*ofilter)(OBJ_P) = (boolean (*)(OBJ_P)) 0;
    boolean show_a,
            do_unpaid = FALSE, do_usedup = FALSE,
            do_blessed = FALSE, do_cursed = FALSE,
            do_uncursed = FALSE, do_buc_unknown = FALSE,
            do_worn = FALSE, verify_All = FALSE;
    int num_buc_types = 0, num_justpicked = 0, clr = NO_COLOR;

    *pick_list = (menu_item *) 0;
    if (!olist)
        return 0;
    if ((qflags & UNPAID_TYPES) != 0 && count_unpaid(olist))
        do_unpaid = TRUE;
    /* caller only passes BILLED_TYPES when there are some used up items
       on shop's bill */
    if ((qflags & BILLED_TYPES) != 0)
        do_usedup = TRUE;
    /* for the 'A' command to remove worn/wielded */
    if ((qflags & WORN_TYPES) != 0) {
        do_worn = TRUE;
        ofilter = is_worn;
    }
    if ((qflags & BUC_BLESSED) != 0
        && count_buc(olist, BUC_BLESSED, ofilter)) {
        do_blessed = TRUE;
        num_buc_types++;
    }
    if ((qflags & BUC_CURSED) != 0
        && count_buc(olist, BUC_CURSED, ofilter)) {
        do_cursed = TRUE;
        num_buc_types++;
    }
    if ((qflags & BUC_UNCURSED) != 0
        && count_buc(olist, BUC_UNCURSED, ofilter)) {
        do_uncursed = TRUE;
        num_buc_types++;
    }
    if ((qflags & BUC_UNKNOWN) != 0
        && count_buc(olist, BUC_UNKNOWN, ofilter)) {
        do_buc_unknown = TRUE;
        num_buc_types++;
    }
    if ((qflags & JUSTPICKED) != 0) {
        num_justpicked = count_justpicked(olist);
    }

    ccount = count_categories(olist, qflags);
    /* no point in actually showing a menu for a single category */
    if (ccount == 1 && !do_unpaid && !do_usedup && num_buc_types <= 1) {
        for (curr = olist; curr; curr = FOLLOW(curr, qflags)) {
            if (ofilter && !(*ofilter)(curr))
                continue;
            break;
        }
        if (curr) {
            *pick_list = (menu_item *) alloc(sizeof(menu_item));
            (*pick_list)->item.a_int = curr->oclass;
            n = 1;
        } else {
            debugpline0("query_category: no single object match");
            n = 0;
        }
        /* early return is ok; there's no temp window yet */
        return n;
    }

    win = create_nhwindow(NHW_MENU);
    start_menu(win, MENU_BEHAVE_STANDARD);

    pack = strcpy(packbuf, flags.inv_order);
    if ((qflags & INCLUDE_VENOM) != 0)
        (void) strkitten(pack, VENOM_CLASS); /* venom is not in inv_order */

    show_a = ((qflags & ALL_TYPES) != 0 && ccount > 1);

    if ((qflags & CHOOSE_ALL) != 0) {
        invlet = 'A';
        any = cg.zeroany;
        any.a_int = 'A';
        add_menu(win, &nul_glyphinfo, &any, invlet, 0, ATR_NONE, clr,
                 /* note: menu_remarm() doesn't pass the CHOOSE_ALL flag,
                    so do_worn handling here is moot */
                 do_worn ? "Auto-select every item being worn or wielded"
                         : "Auto-select every relevant item",
                 MENU_ITEMFLAGS_SKIPINVERT);
        verify_All = (how == PICK_ANY) && ParanoidAutoAll;
        if (!verify_All) {
            if (!ga.A_first_hint++ || iflags.cmdassist)
                add_menu_str(win,
                   "    (ignored unless some other choices are also picked)");
        } else if (show_a) {
            if (!ga.A_second_hint++ || iflags.cmdassist)
                add_menu_str(win,
                      "    (if no other choices are picked, 'a' is implied)");
        }
        /* blank separator */
        add_menu_str(win, "");
    }

    invlet = 'a';
    if (show_a) {
        any = cg.zeroany;
        any.a_int = ALL_TYPES_SELECTED;
        add_menu(win, &nul_glyphinfo, &any, invlet, 0, ATR_NONE, clr,
                 do_worn ? "All worn and wielded types" : "All types",
                 MENU_ITEMFLAGS_SKIPINVERT);
        ++invlet; /* invlet = 'b'; */
    }

    do {
        collected_type_name = FALSE;
        for (curr = olist; curr; curr = FOLLOW(curr, qflags)) {
            if (curr->oclass == *pack) {
                if (ofilter && !(*ofilter)(curr))
                    continue;
                if (!collected_type_name) {
                    int oclass = (int) curr->oclass;

                    any = cg.zeroany;
                    any.a_int = oclass;
                    add_menu(win, &nul_glyphinfo, &any, invlet++,
                             (int) def_oc_syms[oclass].sym, ATR_NONE, clr,
                             let_to_name(*pack, FALSE,
                                         (how != PICK_NONE
                                          && iflags.menu_head_objsym)),
                             MENU_ITEMFLAGS_NONE);
                    collected_type_name = TRUE;
                }
            }
        }
        pack++;
        if (invlet >= 'u') {
            impossible("query_category: too many categories");
            n = 0;
            goto query_done;
        }
    } while (*pack);

    if (do_unpaid || do_usedup
        || do_blessed || do_cursed || do_uncursed || do_buc_unknown
        || num_justpicked) {
        add_menu_str(win, "");
    }

    /* unpaid items if there are any */
    if (do_unpaid) {
        invlet = 'u';
        any = cg.zeroany;
        any.a_int = 'u';
        add_menu(win, &nul_glyphinfo, &any, invlet, 0,
                 ATR_NONE, clr, "Unpaid items", MENU_ITEMFLAGS_SKIPINVERT);
    }
    /* billed items: checked by caller, so always include if BILLED_TYPES */
    if (do_usedup) {
        invlet = 'x';
        any = cg.zeroany;
        any.a_int = 'x';
        add_menu(win, &nul_glyphinfo, &any, invlet, 0, ATR_NONE, clr,
                 "Unpaid items already used up", MENU_ITEMFLAGS_SKIPINVERT);
    }

    /* items with b/u/c/unknown if there are any;
       this cluster of menu entries is in alphabetical order,
       reversing the usual sequence of 'U' and 'C' in BUCX */
    if (do_blessed) {
        invlet = 'B';
        any = cg.zeroany;
        any.a_int = 'B';
        add_menu(win, &nul_glyphinfo, &any, invlet, 0, ATR_NONE, clr,
                 "Items known to be Blessed", MENU_ITEMFLAGS_SKIPINVERT);
    }
    if (do_cursed) {
        invlet = 'C';
        any = cg.zeroany;
        any.a_int = 'C';
        add_menu(win, &nul_glyphinfo, &any, invlet, 0, ATR_NONE, clr,
                 "Items known to be Cursed", MENU_ITEMFLAGS_SKIPINVERT);
    }
    if (do_uncursed) {
        invlet = 'U';
        any = cg.zeroany;
        any.a_int = 'U';
        add_menu(win, &nul_glyphinfo, &any, invlet, 0, ATR_NONE, clr,
                 "Items known to be Uncursed", MENU_ITEMFLAGS_SKIPINVERT);
    }
    if (do_buc_unknown) {
        invlet = 'X';
        any = cg.zeroany;
        any.a_int = 'X';
        add_menu(win, &nul_glyphinfo, &any, invlet, 0, ATR_NONE, clr,
                 "Items of unknown Bless/Curse status",
                 MENU_ITEMFLAGS_SKIPINVERT);
    }
    if (num_justpicked) {
        char tmpbuf[BUFSZ];

        if (num_justpicked == 1)
            Sprintf(tmpbuf, "Just picked up: %s",
                    doname(find_justpicked(olist)));
        else
            Strcpy(tmpbuf, "Items you just picked up");
        invlet = 'P';
        any = cg.zeroany;
        any.a_int = 'P';
        add_menu(win, &nul_glyphinfo, &any, invlet, 0, ATR_NONE, clr,
                 tmpbuf, MENU_ITEMFLAGS_SKIPINVERT);
    }
    end_menu(win, qstr);
    n = select_menu(win, how, pick_list);
    if (n > 0) {
        assert(*pick_list != NULL);
    }

    /* handle ParanoidAutoAll by confirming 'A' choice if present */
    if (n > 0 && verify_All) {
        int i, j;

        for (i = 0; i < n; ++i)
            if ((*pick_list)[i].item.a_int == 'A') {
                /* ParanoidAutoAll is set (otherwise verify_All is false);
                   if ParanoidConfirm is also set, require "yes" rather than
                   just "y" to accept (and "no" rather than "n" to decline;
                   accepts "quit" and ESC without converting them to 'n') */
                switch (paranoid_ynq(ParanoidConfirm,
                                     "Really autoselect All?", TRUE)) {
                case 'y':
                    /* yes => honor Auto-select All */
                    break;
                case 'n':
                    /* no => remove 'A' from the list; if that would make
                       it empty then replace with 'a' */
                    if (n > 1) {
                        for (j = i + 1; j < n; ++j)
                            (*pick_list)[j - 1] = (*pick_list)[j];
                        --n;
                        break; /* from switch */
                    } else if ((qflags & ALL_TYPES) != 0) {
                        /* 'A' was the only choice; convert it to 'a' and
                           then let the next menu offer a choice of all */
                        (*pick_list)[0].item.a_int = ALL_TYPES_SELECTED;
                        /* assert( n == 1 ); */
                        break; /* from switch */
                    }
                    FALLTHROUGH;
                    /*FALLTHRU*/
                case 'q':
                default:
                    /* quit | ESC => cancel, no Auto-select and no 2nd menu */
                    n = 0;
                    free((genericptr_t) *pick_list), *pick_list = 0;
                    break;
                }
                break; /* from for => goto query_done; */
            }
    } else if (n == 1 && !verify_All && (*pick_list)[0].item.a_int == 'A') {
        /* without paranoid_confirm:A, choosing 'A' by itself is rejected */
        n = 0;
        free((genericptr_t) *pick_list), *pick_list = 0;
        /* the menu entry description is "Auto-select every relevant item"
           [not sure whether issuing a message here is a good idea...] */
        pline("No relevant items selected.");
    }
 query_done:
    destroy_nhwindow(win);
    if (n < 0) /* closed menu with ESC */
        n = 0; /* callers don't expect -1 */
    return n;
}

staticfn int
count_categories(struct obj *olist, int qflags)
{
    char *pack;
    boolean counted_category;
    int ccount = 0;
    struct obj *curr;
    boolean do_worn = (qflags & WORN_TYPES) != 0;

    pack = flags.inv_order;
    do {
        counted_category = FALSE;
        for (curr = olist; curr; curr = FOLLOW(curr, qflags)) {
            if (curr->oclass == *pack) {
                if (do_worn && !(curr->owornmask
                                 & (W_ARMOR | W_ACCESSORY | W_WEAPONS)))
                    continue;
                if (!counted_category) {
                    ccount++;
                    counted_category = TRUE;
                }
            }
        }
        pack++;
    } while (*pack);
    return ccount;
}

/*
 *  How much the weight of the given container will change when the given
 *  object is removed from it.  Use before and after weight amounts rather
 *  than trying to match the calculation used by weight() in mkobj.c.
 */
staticfn int
delta_cwt(struct obj *container, struct obj *obj)
{
    struct obj **prev;
    int owt, nwt;

    if (container->otyp != BAG_OF_HOLDING)
        return obj->owt;

    owt = nwt = container->owt;
    /* find the object so that we can remove it */
    for (prev = &container->cobj; *prev; prev = &(*prev)->nobj)
        if (*prev == obj)
            break;
    if (!*prev) {
        panic("delta_cwt: obj not inside container?");
    } else {
        /* temporarily remove the object and calculate resulting weight */
        *prev = obj->nobj;
        nwt = weight(container);
        *prev = obj; /* put the object back; obj->nobj is still valid */
    }
    return owt - nwt;
}

/* could we carry `obj'? if not, could we carry some of it/them? */
staticfn long
carry_count(struct obj *obj,            /* object to pick up... */
            struct obj *container,      /* ...bag it is coming out of */
            long count,
            boolean telekinesis,
            int *wt_before, int *wt_after)
{
    boolean adjust_wt = container && carried(container),
            is_gold = obj->oclass == COIN_CLASS;
    int wt, iw, ow, oow;
    long qq, savequan, umoney;
    unsigned saveowt;
    const char *verb, *prefx1, *prefx2, *suffx;
    char obj_nambuf[BUFSZ], where[BUFSZ];

    savequan = obj->quan;
    saveowt = obj->owt;
    umoney = money_cnt(gi.invent);
    iw = max_capacity();

    if (count != savequan) {
        obj->quan = count;
        obj->owt = (unsigned) weight(obj);
    }
    wt = iw + (int) obj->owt;
    if (adjust_wt)
        wt -= delta_cwt(container, obj);
    /* This will go with silver+copper & new gold weight */
    if (is_gold) /* merged gold might affect cumulative weight */
        wt -= (GOLD_WT(umoney) + GOLD_WT(count) - GOLD_WT(umoney + count));
    if (count != savequan) {
        obj->quan = savequan;
        obj->owt = saveowt;
    }
    *wt_before = iw;
    *wt_after = wt;

    if (wt < 0)
        return count;

    /* see how many we can lift */
    if (is_gold) {
        iw -= (int) GOLD_WT(umoney);
        if (!adjust_wt) {
            qq = GOLD_CAPACITY((long) iw, umoney);
        } else {
            oow = 0;
            qq = 50L - (umoney % 100L) - 1L;
            if (qq < 0L)
                qq += 100L;
            for (; qq <= count; qq += 100L) {
                obj->quan = qq;
                obj->owt = (unsigned) GOLD_WT(qq);
                ow = (int) GOLD_WT(umoney + qq);
                ow -= delta_cwt(container, obj);
                if (iw + ow >= 0)
                    break;
                oow = ow;
            }
            iw -= oow;
            qq -= 100L;
        }
        if (qq < 0L)
            qq = 0L;
        else if (qq > count)
            qq = count;
        wt = iw + (int) GOLD_WT(umoney + qq);
    } else if (count > 1 || count < obj->quan) {
        /*
         * Ugh. Calc num to lift by changing the quan of the
         * object and calling weight.
         *
         * This works for containers only because containers
         * don't merge.  -dean
         */
        for (qq = 1L; qq <= count; qq++) {
            obj->quan = qq;
            obj->owt = (unsigned) (ow = weight(obj));
            if (adjust_wt)
                ow -= delta_cwt(container, obj);
            if (iw + ow >= 0)
                break;
            wt = iw + ow;
        }
        --qq;
    } else {
        /* there's only one, and we can't lift it */
        qq = 0L;
    }
    obj->quan = savequan;
    obj->owt = saveowt;

    if (qq < count) {
        /* some message will be given */
        Strcpy(obj_nambuf, doname(obj));
        if (container) {
            Sprintf(where, "in %s", the(xname(container)));
            verb = "carry";
        } else {
            Strcpy(where, "lying here");
            verb = telekinesis ? "acquire" : "lift";
        }
    } else {
        /* lint suppression */
        *obj_nambuf = *where = '\0';
        verb = "";
    }
    /* we can carry qq of them */
    if (qq > 0) {
        if (qq < count)
            You("can only %s %s of the %s %s.", verb,
                (qq == 1L) ? "one" : "some", obj_nambuf, where);
        *wt_after = wt;
        return qq;
    }

    if (!container)
        Strcpy(where, "here"); /* slightly shorter form */
    if (gi.invent || umoney) {
        prefx1 = "you cannot ";
        prefx2 = "";
        suffx = " any more";
    } else {
        prefx1 = (obj->quan == 1L) ? "it " : "even one ";
        prefx2 = "is too heavy for you to ";
        suffx = "";
    }
    There("%s %s %s, but %s%s%s%s.", otense(obj, "are"), obj_nambuf, where,
          prefx1, prefx2, verb, suffx);

    /* *wt_after = iw; */
    return 0L;
}

/* determine whether character is able and player is willing to carry `obj' */
staticfn int
lift_object(
    struct obj *obj,       /* object to pick up... */
    struct obj *container, /* ...bag it's coming out of */
    long *cnt_p,
    boolean telekinesis)
{
    int result, old_wt, new_wt, prev_encumbr, next_encumbr;

    if (obj->otyp == BOULDER && Sokoban) {
        You("cannot get your %s around this %s.", body_part(HAND),
            xname(obj));
        return -1;
    }
    /* override weight consideration for loadstone picked up by anybody
       and for boulder picked up by hero poly'd into a giant; override
       availability of open inventory slot iff not already carrying one */
    if (obj->otyp == LOADSTONE
        || (obj->otyp == BOULDER && throws_rocks(gy.youmonst.data))) {
        if (inv_cnt(FALSE) < invlet_basic || !carrying(obj->otyp)
            || merge_choice(gi.invent, obj))
            return 1; /* lift regardless of current situation */
        /* if we reach here, we're out of slots and already have at least
           one of these, so treat this one more like a normal item
           [this was using simpleonames(obj) for shortest description, but
           that's suboptimal for loadstones because it omits user-assigned
           type name which is something of interest for gray stones] */
        You("are carrying too much stuff to pick up %s %s.",
            (obj->quan == 1L) ? "another" : "more", xname(obj));
        return -1;
    }

    *cnt_p = carry_count(obj, container, *cnt_p, telekinesis,
                         &old_wt, &new_wt);
    if (*cnt_p < 1L) {
        result = -1; /* nothing lifted */
    } else if (obj->oclass != COIN_CLASS
               /* [exception for gold coins will have to change
                   if silver/copper ones ever get implemented] */
               && inv_cnt(FALSE) >= invlet_basic
               && !merge_choice(gi.invent, obj)) {
        /* if there is some gold here (and we haven't already skipped it),
           we aren't limited by the 52 item limit for it, but caller and
           "grandcaller" aren't prepared to skip stuff and then pickup
           just gold, so the best we can do here is vary the message */
        Your("knapsack cannot accommodate any more items%s.",
             /* floor follows by nexthere, otherwise container so by nobj */
             nxtobj(obj, GOLD_PIECE, (boolean) (obj->where == OBJ_FLOOR))
                 ? " (except gold)" : "");
        result = -1; /* nothing lifted */
    } else {
        result = 1;
        prev_encumbr = near_capacity();
        if (prev_encumbr < flags.pickup_burden)
            prev_encumbr = flags.pickup_burden;
        next_encumbr = calc_capacity(new_wt - old_wt);
        if (next_encumbr > prev_encumbr) {
            if (telekinesis) {
                result = 0; /* don't lift */
            } else {
                char qbuf[BUFSZ];
                long savequan = obj->quan;

                obj->quan = *cnt_p;
                Sprintf(qbuf, "%s %s ",
                        (next_encumbr >= EXT_ENCUMBER) ? overloadpfx
                        : (next_encumbr >= HVY_ENCUMBER) ? nearloadpfx
                          : (next_encumbr >= MOD_ENCUMBER) ? moderateloadpfx
                            : slightloadpfx,
                        !container ? "lifting" : "removing");
                (void) safe_qbuf(qbuf, qbuf, ".  Continue?", obj, doname,
                                 ansimpleoname, something);
                obj->quan = savequan;
                switch (ynq(qbuf)) {
                case 'q':
                    result = -1;
                    break;
                case 'n':
                    result = 0;
                    break;
                default:
                    break; /* 'y' => result == 1 */
                }
                clear_nhwindow(WIN_MESSAGE);
            }
        }
    }

    if (obj->otyp == SCR_SCARE_MONSTER && result <= 0 && !container)
        obj->spe = 0;
    return result;
}

/*
 * Pick up <count> of obj from the ground and add it to the hero's inventory.
 * Returns -1 if caller should break out of its loop, 0 if nothing picked
 * up, 1 if otherwise.
 */
int
pickup_object(
    struct obj *obj,
    long count, /* if non-zero, pick up a subset of this amount */
    boolean telekinesis) /* not picking it up directly by hand */
{
    int res;

    if (obj->quan < count) {
        impossible("pickup_object: count %ld > quan %ld?", count, obj->quan);
        return 0;
    }

    /* In case of auto-pickup, where we haven't had a chance
       to look at it yet; affects docall(SCR_SCARE_MONSTER). */
    if (!Blind)
        observe_object(obj);

    if (obj == uchain) { /* do not pick up attached chain */
        return 0;
    } else if (obj->where == OBJ_MINVENT && obj->owornmask != 0L
               && engulfing_u(obj->ocarry)) {
        You_cant("pick %s up.", ysimple_name(obj));
        return 0;
    } else if (obj->oartifact && !touch_artifact(obj, &gy.youmonst)) {
        return 0;
    } else if (obj->otyp == CORPSE) {
        if (fatal_corpse_mistake(obj, telekinesis)
            || rider_corpse_revival(obj, telekinesis))
            return -1;
    } else if (obj->otyp == SCR_SCARE_MONSTER) {
        int old_wt, new_wt;

        /* process a count before altering/deleting scrolls;
           tricky because being unable to lift full stack imposes an
           implicit count; unliftable ones should be treated as if
           the count excluded them so that they don't change state */
        if ((count = carry_count(obj, (struct obj *) 0,
                                 count ? count : obj->quan,
                                 FALSE, &old_wt, &new_wt)) < 1L)
            return -1; /* couldn't even pick up 1, so effectively untouched */
        /* all current callers handle a new object sanely when traversing
           a list; other half of a split will be left as-is and whatever
           already follows 'obj' will still be processed next */
        if (count > 0L && count < obj->quan)
            obj = splitobj(obj, count);

        if (obj->blessed) {
            unbless(obj);
        } else if (!obj->spe && !obj->cursed) {
            obj->spe = 1;
        } else {
            pline_The("scroll%s %s to dust as you %s %s up.", plur(obj->quan),
                      otense(obj, "turn"), telekinesis ? "raise" : "pick",
                      (obj->quan == 1L) ? "it" : "them");
            trycall(obj);
            useupf(obj, obj->quan);
            return 1; /* tried to pick something up and failed, but
                         don't want to terminate pickup loop yet   */
        }
    }

    /* obj has either already passed autopick_testobj or we are explicitly
       picking it off the floor, so addinv() will override obj->how_lost;
       otherwise we couldn't pick up a thrown, stolen, or dropped item that
       was split off from a carried stack even while still carrying the
       rest of the stack unless we have at least one free slot available */
    res = lift_object(obj, (struct obj *) 0, &count, telekinesis);
    if (res <= 0)
        return res;

    /* What's left of the special case for gold :-) */
    if (obj->oclass == COIN_CLASS)
        disp.botl = TRUE;
    if (obj->quan != count && obj->otyp != LOADSTONE)
        obj = splitobj(obj, count);

    obj = pick_obj(obj);

    if (uwep && uwep == obj)
        gm.mrg_to_wielded = TRUE;
    pickup_prinv(obj, count, "lifting");
    if (obj->ghostly)
        fix_ghostly_obj(obj);
    gm.mrg_to_wielded = FALSE;
    return 1;
}

/*
 * Do the actual work of picking otmp from the floor or monster's interior
 * and putting it in the hero's inventory.  Take care of billing.  Return a
 * pointer to the object where otmp ends up.  This may be different
 * from otmp because of merging.
 */
struct obj *
pick_obj(struct obj *otmp)
{
    struct obj *result;
    coordxy ox, oy;
    boolean robshop, fromfloor = otmp->where == OBJ_FLOOR;

    /* otmp is either on the floor or in an engulfer's inventory; for the
       latter, its <ox,oy> probably won't be set */
    (void) get_obj_location(otmp, &ox, &oy, 0);

    robshop = (!u.uswallow && otmp != uball && costly_spot(ox, oy));
    obj_extract_self(otmp);
    if (fromfloor)
        newsym(ox, oy);

    /* for shop items, addinv() needs to be after addtobill() (so that
       object merger can take otmp->unpaid into account) but before
       remote_robbery() (which calls rob_shop() which calls setpaid()
       after moving costs of unpaid items to shop debt; setpaid()
       calls clear_unpaid() for lots of object chains, but 'otmp' isn't
       on any of those between obj_extract_self() and addinv(); for
       3.6.0, 'otmp' remained flagged as an unpaid item in inventory
       and triggered impossible() every time inventory was examined) */
    if (robshop) {
        char saveushops[5], fakeshop[2];

        /* addtobill cares about your location rather than the object's;
           usually they'll be the same, but not when using telekinesis
           (if ever implemented) or a grappling hook */
        Strcpy(saveushops, u.ushops);
        fakeshop[0] = *in_rooms(ox, oy, SHOPBASE);
        fakeshop[1] = '\0';
        Strcpy(u.ushops, fakeshop);
        /* sets obj->unpaid if necessary */
        addtobill(otmp, TRUE, FALSE, FALSE);
        Strcpy(u.ushops, saveushops);
        robshop = otmp->unpaid && !strchr(u.ushops, *fakeshop);
    }

    result = addinv(otmp);
    /* if you're taking a shop item from outside the shop, make shk notice */
    if (robshop)
        remote_burglary(ox, oy);

    return result;
}

/* pickup_object()/out_container() helper;
   print an added-to-invent message for current object, limiting feedback
   about encumbrance to the first item which causes that to change */
staticfn void
pickup_prinv(
    struct obj *obj,
    long count,
    const char *verb)
{
    char pbuf[QBUFSZ];
    const char *prefix;
    int nearload = near_capacity();

    pbuf[0] = '\0';
    if (nearload == gp.pickup_encumbrance) {
        prefix = (char *) 0;
    } else {
        prefix = (nearload >= EXT_ENCUMBER) ? overloadpfx
                 : (nearload >= HVY_ENCUMBER) ? nearloadpfx
                   : (nearload >= MOD_ENCUMBER) ? moderateloadpfx
                     : (nearload >= SLT_ENCUMBER) ? slightloadpfx
                       : (char *) 0;
        gp.pickup_encumbrance = nearload;
    }
    if (prefix)
        Sprintf(pbuf, "%s %s", prefix, verb);

    prinv(pbuf, obj, count);
}

/*
 * prints a message if encumbrance changed since the last check
 */
void
encumber_msg(void)
{
    int newcap = near_capacity();

    if (go.oldcap < newcap) {
        switch (newcap) {
        case 1:
            Your("movements are slowed slightly because of your load.");
            break;
        case 2:
            You("rebalance your load.  Movement is difficult.");
            break;
        case 3:
            You("%s under your heavy load.  Movement is very hard.",
                stagger(gy.youmonst.data, "stagger"));
            break;
        default:
            You("%s move a handspan with this load!",
                newcap == 4 ? "can barely" : "can't even");
            break;
        }
        disp.botl = TRUE;
    } else if (go.oldcap > newcap) {
        switch (newcap) {
        case 0:
            Your("movements are now unencumbered.");
            break;
        case 1:
            Your("movements are only slowed slightly by your load.");
            break;
        case 2:
            You("rebalance your load.  Movement is still difficult.");
            break;
        case 3:
            You("%s under your load.  Movement is still very hard.",
                stagger(gy.youmonst.data, "stagger"));
            break;
        }
        disp.botl = TRUE;
    }

    go.oldcap = newcap;
}

/* Is there a container at x,y. Optional: return count of containers at x,y */
int
container_at(coordxy x, coordxy y, boolean countem)
{
    struct obj *cobj, *nobj;
    int container_count = 0;

    for (cobj = svl.level.objects[x][y]; cobj; cobj = nobj) {
        nobj = cobj->nexthere;
        if (Is_container(cobj)) {
            container_count++;
            if (!countem)
                break;
        }
    }
    return container_count;
}

staticfn boolean
able_to_loot(
    coordxy x, coordxy y,
    boolean looting) /* loot vs tip */
{
    const char *verb = looting ? "loot" : "tip";
    struct trap *t = t_at(x, y);

    if (!can_reach_floor(t && is_pit(t->ttyp))) {
        if (u.usteed && P_SKILL(P_RIDING) < P_BASIC)
            rider_cant_reach(); /* not skilled enough to reach */
        else
            cant_reach_floor(x, y, FALSE, TRUE, FALSE);
        return FALSE;
    } else if ((is_pool(x, y) && (looting || !Underwater)) || is_lava(x, y)) {
        /* at present, can't loot in water even when Underwater;
           can tip underwater, but not when over--or stuck in--lava */
        You("cannot %s things that are deep in the %s.", verb,
            hliquid(is_lava(x, y) ? "lava" : "water"));
        return FALSE;
    } else if (nolimbs(gy.youmonst.data)) {
        pline("Without limbs, you cannot %s anything.", verb);
        return FALSE;
    } else if (looting && !freehand()) {
        pline("Without a free %s, you cannot loot anything.",
              body_part(HAND));
        return FALSE;
    }
    return TRUE;
}

staticfn boolean
mon_beside(coordxy x, coordxy y)
{
    int i, j;
    coordxy nx, ny;

    for (i = -1; i <= 1; i++)
        for (j = -1; j <= 1; j++) {
            nx = x + i;
            ny = y + j;
            if (isok(nx, ny) && MON_AT(nx, ny))
                return TRUE;
        }
    return FALSE;
}

staticfn int
do_loot_cont(
    struct obj **cobjp,
    int cindex, /* index of this container (1..N)... */
    int ccount) /* ...number of them (N) */
{
    struct obj *cobj = *cobjp;

    if (!cobj)
        return ECMD_OK;
    if (cobj->olocked) {
        int res = ECMD_OK;

#if 0
        if (ccount < 2 && (svl.level.objects[cobj->ox][cobj->oy] == cobj))
            pline("%s locked.",
                  cobj->lknown ? "It is" : "Hmmm, it turns out to be");
        else
#endif
        if (cobj->lknown)
            pline("%s is locked.", The(xname(cobj)));
        else
            pline("Hmmm, %s turns out to be locked.", the(xname(cobj)));
        cobj->lknown = 1;

        if (flags.autounlock) {
            struct obj *otmp, *unlocktool = 0;
            coordxy ox = cobj->ox, oy = cobj->oy;

            u.dz = 0; /* might be non-zero from previous command since
                       * #loot isn't a move command; pick_lock() cares */
            /* if both the untrap and apply_key bits are set, untrap
               attempt will be performed first but we need to set up
               unlocktool in case "check for trap?" is declined */
            if (((flags.autounlock & AUTOUNLOCK_APPLY_KEY) != 0
                 && (unlocktool = autokey(TRUE)) != 0)
                || (flags.autounlock & AUTOUNLOCK_UNTRAP) != 0) {
                /* pass ox and oy to avoid direction prompt */
                if (pick_lock(unlocktool, ox, oy, cobj))
                    res = ECMD_TIME;
                /* attempting to untrap or unlock might trigger a trap
                   which destroys 'cobj'; inform caller if that happens */
                for (otmp = svl.level.objects[ox][oy]; otmp;
                     otmp = otmp->nexthere)
                    if (otmp == cobj)
                        break;
                if (!otmp)
                    *cobjp = (struct obj *) 0;
                return res;
            }
            if ((flags.autounlock & AUTOUNLOCK_FORCE) != 0
                && res != ECMD_TIME
                && ccount == 1 && u_have_forceable_weapon()) {
                /* single container, and we could #force it open... */
                /* note: doforce asks for confirmation */
                cmdq_add_ec(CQ_CANNED, doforce);
                ga.abort_looting = TRUE;
            }
        }
        return res;
    }
    cobj->lknown = 1; /* floor container, so no need for update_inventory() */

    if (cobj->otyp == BAG_OF_TRICKS) {
        int tmp;

        You("carefully open %s...", the(xname(cobj)));
        pline("It develops a huge set of teeth and bites you!");
        tmp = rnd(10);
        losehp(Maybe_Half_Phys(tmp), "carnivorous bag", KILLED_BY_AN);
        makeknown(BAG_OF_TRICKS);
        ga.abort_looting = TRUE;
        return ECMD_TIME;
    }
    return use_container(cobjp, FALSE, (boolean) (cindex < ccount));
}

/* #loot extended command */
int
doloot(void)
{
    int res;

    gl.loot_reset_justpicked = TRUE;
    res = doloot_core();
    gl.loot_reset_justpicked = FALSE;
    return res;
}

/* loot a container on the floor or loot saddle from mon. */
staticfn int
doloot_core(void)
{
    struct obj *cobj, *nobj;
    int c = -1;
    int timepassed = 0;
    coord cc;
    boolean underfoot = TRUE;
    const char *dont_find_anything = "don't find anything";
    struct monst *mtmp;
    int prev_inquiry = 0;
    boolean prev_loot = FALSE;
    int num_conts = 0;
    int clr = NO_COLOR;

    ga.abort_looting = FALSE;

    if (check_capacity((char *) 0)) {
        /* "Can't do that while carrying so much stuff." */
        return ECMD_OK;
    }
    if (nohands(gy.youmonst.data)) {
        You("have no hands!"); /* not `body_part(HAND)' */
        return ECMD_OK;
    }
    if (Confusion) {
        if (rn2(6) && reverse_loot())
            return ECMD_TIME;
        if (rn2(2)) {
            pline("Being confused, you find nothing to loot.");
            return ECMD_TIME; /* costs a turn */
        }             /* else fallthrough to normal looting */
    }
    cc.x = u.ux;
    cc.y = u.uy;

    if (iflags.menu_requested)
        goto lootmon;

 lootcont:
    if ((num_conts = container_at(cc.x, cc.y, TRUE)) > 0) {
        boolean anyfound = FALSE;

        if (!able_to_loot(cc.x, cc.y, TRUE))
            return ECMD_OK;

        if (Blind && !uarmg) {
            /* if blind and without gloves, attempting to #loot at the
               location of a cockatrice corpse is fatal before asking
               whether to manipulate any containers */
            for (nobj = sobj_at(CORPSE, cc.x, cc.y); nobj;
                 nobj = nxtobj(nobj, CORPSE, TRUE))
                if (will_feel_cockatrice(nobj, FALSE)) {
                    feel_cockatrice(nobj, FALSE);
                    /* if life-saved (or poly'd into stone golem),
                       terminate attempt to loot */
                    return ECMD_TIME;
                }
        }

        if (num_conts > 1) {
            /* use a menu to loot many containers */
            int n, i;
            winid win;
            anything any;
            menu_item *pick_list = (menu_item *) 0;

            any.a_void = 0;
            win = create_nhwindow(NHW_MENU);
            start_menu(win, MENU_BEHAVE_STANDARD);

            for (cobj = svl.level.objects[cc.x][cc.y]; cobj;
                 cobj = cobj->nexthere)
                if (Is_container(cobj)) {
                    any.a_obj = cobj;
                    add_menu(win, &nul_glyphinfo, &any, 0, 0, ATR_NONE, clr,
                             doname(cobj), MENU_ITEMFLAGS_NONE);
                }
            end_menu(win, "Loot which containers?");
            n = select_menu(win, PICK_ANY, &pick_list);
            destroy_nhwindow(win);

            if (n > 0) {
                for (i = 1; i <= n; i++) {
                    cobj = pick_list[i - 1].item.a_obj;
                    timepassed |= do_loot_cont(&cobj, i, n);
                    if (ga.abort_looting) {
                        /* chest trap or magic bag explosion or <esc> */
                        free((genericptr_t) pick_list);
                        return (timepassed ? ECMD_TIME : ECMD_OK);
                    }
                }
                free((genericptr_t) pick_list);
            }
            if (n != 0)
                c = 'y';
        } else {
            for (cobj = svl.level.objects[cc.x][cc.y]; cobj; cobj = nobj) {
                nobj = cobj->nexthere;

                if (Is_container(cobj)) {
                    anyfound = TRUE;
                    timepassed |= do_loot_cont(&cobj, 1, 1);
                    if (ga.abort_looting)
                        /* chest trap or magic bag explosion or <esc> */
                        return (timepassed ? ECMD_TIME : ECMD_OK);
                }
            }
            if (anyfound)
                c = 'y';
        }
    } else if (IS_GRAVE(levl[cc.x][cc.y].typ)) {
        You("need to dig up the grave to effectively loot it...");
    }

    /*
     * 3.3.1 introduced directional looting for some things.
     */
 lootmon:
    if (c != 'y' && (mon_beside(u.ux, u.uy) || iflags.menu_requested)) {
        boolean looted_mon = FALSE;
        if (!get_adjacent_loc("Loot in what direction?",
                              "Invalid loot location", u.ux, u.uy, &cc))
            return ECMD_OK;
        underfoot = u_at(cc.x, cc.y);
        if (underfoot && container_at(cc.x, cc.y, FALSE))
            goto lootcont;
        if (u.dz < 0) {
            You("%s to loot on the %s.", dont_find_anything,
                ceiling(cc.x, cc.y));
            return ECMD_TIME;
        }
        mtmp = m_at(cc.x, cc.y);
        if (mtmp) {
            timepassed = loot_mon(mtmp, &prev_inquiry, &prev_loot);
            if (timepassed)
                looted_mon = TRUE;
        }
        /* always use a turn when choosing a direction is impaired,
           even if you've successfully targeted a saddled creature
           and then answered "no" to the "remove its saddle?" prompt */
        if (Confusion || Stunned)
            timepassed = 1;

        /* Preserve pre-3.3.1 behavior for containers.
         * Adjust this if-block to allow container looting
         * from one square away to change that in the future.
         */
        if (!looted_mon) {
            if (!underfoot && container_at(cc.x, cc.y, FALSE)) {
                if (mtmp) {
                    You_cant("loot anything %sthere with %s in the way.",
                             prev_inquiry ? "else " : "", mon_nam(mtmp));
                    return (timepassed ? ECMD_TIME : ECMD_OK);
                } else {
                    You("have to be at a container to loot it.");
                }
            } else {
                You("%s %s%shere to loot.", dont_find_anything,
                    (prev_inquiry || prev_loot) ? "else " : "",
                    !underfoot ? "t" : "");
                return (timepassed ? ECMD_TIME : ECMD_OK);
            }
        }
    } else if (c != 'y' && c != 'n') {
        You("%s %s to loot.", dont_find_anything,
            underfoot ? "here" : "there");
    }
    return (timepassed ? ECMD_TIME : ECMD_OK);
}

/* called when attempting to #loot while confused */
staticfn boolean
reverse_loot(void)
{
    struct obj *goldob = 0, *coffers, *otmp, boxdummy;
    struct monst *mon;
    long contribution;
    int n, x = u.ux, y = u.uy;

    if (!rn2(3)) {
        /* n objects: 1/(n+1) chance per object, 1/(n+1) to fall off end */
        for (n = inv_cnt(TRUE), otmp = gi.invent; otmp;
             --n, otmp = otmp->nobj)
            if (!rn2(n + 1)) {
                prinv("You find old loot:", otmp, 0L);
                return TRUE;
            }
        return FALSE;
    }

    /* find a money object to mess with */
    for (goldob = gi.invent; goldob; goldob = goldob->nobj)
        if (goldob->oclass == COIN_CLASS) {
            contribution = ((long) rnd(5) * goldob->quan + 4L) / 5L;
            if (contribution < goldob->quan)
                goldob = splitobj(goldob, contribution);
            break;
        }
    if (!goldob)
        return FALSE;

    /* gold might be quivered; dropping would un-wear it, but freeinv()
       expects caller to do that; do so now */
    remove_worn_item(goldob, FALSE);

    if (!IS_THRONE(levl[x][y].typ)) {
        dropx(goldob);
        /* the dropped gold might have fallen to lower level */
        if (g_at(x, y))
            pline("Ok, now there is loot here.");
    } else {
        /* find original coffers chest if present, otherwise use nearest */
        otmp = 0;
        for (coffers = fobj; coffers; coffers = coffers->nobj)
            if (coffers->otyp == CHEST) {
                if (coffers->spe == 2)
                    break; /* a throne room chest */
                if (!otmp || (distu(coffers->ox, coffers->oy)
                              < distu(otmp->ox, otmp->oy)))
                    otmp = coffers; /* remember closest ordinary chest */
            }
        if (!coffers)
            coffers = otmp;

        if (coffers) {
            SetVoice((struct monst *) 0, 0, 80, 0);
            verbalize("Thank you for your contribution to reduce the debt.");
            freeinv(goldob);
            (void) add_to_container(coffers, goldob);
            coffers->owt = weight(coffers);
            coffers->cknown = 0;
            if (!coffers->olocked) {
                boxdummy = cg.zeroobj, boxdummy.otyp = SPE_WIZARD_LOCK;
                (void) boxlock(coffers, &boxdummy);
            }
        } else if (levl[x][y].looted != T_LOOTED
                   && (mon = makemon(courtmon(), x, y, NO_MM_FLAGS)) != 0) {
            freeinv(goldob);
            add_to_minv(mon, goldob);
            pline("The exchequer accepts your contribution.");
            if (!rn2(10))
                levl[x][y].looted = T_LOOTED;
        } else {
            You("drop %s.", doname(goldob));
            dropx(goldob);
        }
    }
    return TRUE;
}

/* loot_mon() returns amount of time passed.
 */
int
loot_mon(struct monst *mtmp, int *passed_info, boolean *prev_loot)
{
    int c = -1;
    int timepassed = 0;
    struct obj *otmp;
    char qbuf[QBUFSZ];

    /* 3.3.1 introduced the ability to remove saddle from a steed.
     *  *passed_info is set to TRUE if a loot query was given.
     *  *prev_loot is set to TRUE if something was actually acquired in here.
     */
    if (mtmp && mtmp != u.usteed && (otmp = which_armor(mtmp, W_SADDLE))) {
        if (passed_info)
            *passed_info = 1;
        Sprintf(qbuf, "Do you want to remove the saddle from %s?",
                x_monnam(mtmp, ARTICLE_THE, (char *) 0,
                         SUPPRESS_SADDLE, FALSE));
        if ((c = yn_function(qbuf, ynqchars, 'n', TRUE)) == 'y') {
            if (nolimbs(gy.youmonst.data)) {
                You_cant("do that without limbs."); /* not body_part(HAND) */
                return 0;
            }
            if (otmp->cursed) {
                You("can't.  The saddle seems to be stuck to %s.",
                    x_monnam(mtmp, ARTICLE_THE, (char *) 0,
                             SUPPRESS_SADDLE, FALSE));
                /* the attempt costs you time */
                return 1;
            }
            extract_from_minvent(mtmp, otmp, TRUE, FALSE);
            if (flags.verbose)
                You("take %s off of %s.",
                    thesimpleoname(otmp), mon_nam(mtmp));
            otmp = hold_another_object(otmp, "You drop %s!", doname(otmp),
                                       (const char *) 0);
            nhUse(otmp);
            timepassed = rnd(3);
            if (prev_loot)
                *prev_loot = TRUE;
        } else if (c == 'q') {
            return 0;
        }
    }
    /* 3.4.0 introduced ability to pick things up from swallower's stomach */
    if (u.uswallow) {
        int count = passed_info ? *passed_info : 0;

        timepassed = pickup(count);
    }
    return timepassed;
}

/*
 * Decide whether an object being placed into a magic bag will cause
 * it to explode.  If the object is a bag itself, check recursively.
 */
staticfn boolean
mbag_explodes(struct obj *obj, int depthin)
{
    /* these won't cause an explosion when they're empty */
    if ((obj->otyp == WAN_CANCELLATION || obj->otyp == BAG_OF_TRICKS)
        && obj->spe <= 0)
        return FALSE;

    /* odds: 1/1, 2/2, 3/4, 4/8, 5/16, 6/32, 7/64, 8/128, 9/128, 10/128,... */
    if ((Is_mbag(obj) || obj->otyp == WAN_CANCELLATION)
        && (rn2(1 << (depthin > 7 ? 7 : depthin)) <= depthin))
        return TRUE;
    else if (Has_contents(obj)) {
        struct obj *otmp;

        for (otmp = obj->cobj; otmp; otmp = otmp->nobj)
            if (mbag_explodes(otmp, depthin + 1))
                return TRUE;
    }
    return FALSE;
}

staticfn boolean
is_boh_item_gone(void)
{
    return (boolean) (!rn2(13));
}

/* Scatter most of Bag of holding contents around.  Some items will be
   destroyed with the same chance as looting a cursed bag. */
staticfn void
do_boh_explosion(struct obj *boh, boolean on_floor)
{
    struct obj *otmp, *nobj;

    boh->in_use = 1; /* in case scatter() leads to bones creation */
    for (otmp = boh->cobj; otmp; otmp = nobj) {
        nobj = otmp->nobj;
        if (is_boh_item_gone()) {
            obj_extract_self(otmp);
            mbag_item_gone(!on_floor, otmp, TRUE);
        } else {
            otmp->ox = u.ux, otmp->oy = u.uy;
            (void) scatter(u.ux, u.uy, 4, MAY_HIT | MAY_DESTROY, otmp);
        }
    }
    /* boh is about to be deleted so no need to reset its in_use flag here */
}

staticfn long
boh_loss(struct obj *container, boolean held)
{
    /* sometimes toss objects if a cursed magic bag */
    if (Is_mbag(container) && container->cursed && Has_contents(container)) {
        long loss = 0L;
        struct obj *curr, *otmp;

        for (curr = container->cobj; curr; curr = otmp) {
            otmp = curr->nobj;
            if (is_boh_item_gone()) {
                obj_extract_self(curr);
                loss += mbag_item_gone(held, curr, FALSE);
            }
        }
        return loss;
    }
    return 0;
}

/* Returns: -1 to stop, 1 item was inserted, 0 item was not inserted. */
staticfn int
in_container(struct obj *obj)
{
    boolean floor_container = !carried(gc.current_container);
    boolean was_unpaid = FALSE;
    char buf[BUFSZ];

    if (!gc.current_container) {
        impossible("<in> no gc.current_container?");
        return 0;
    } else if (obj == uball || obj == uchain) {
        You("must be kidding.");
        return 0;
    } else if (obj == gc.current_container) {
        pline("That would be an interesting topological exercise.");
        return 0;
    } else if (obj->owornmask & (W_ARMOR | W_ACCESSORY)) {
        Norep("You cannot %s %s you are wearing.",
              Icebox ? "refrigerate" : "stash", something);
        return 0;
    } else if ((obj->otyp == LOADSTONE) && obj->cursed) {
        set_bknown(obj, 1);
        pline_The("stone%s won't leave your person.", plur(obj->quan));
        return 0;
    } else if (obj->otyp == AMULET_OF_YENDOR
               || obj->otyp == CANDELABRUM_OF_INVOCATION
               || obj->otyp == BELL_OF_OPENING
               || obj->otyp == SPE_BOOK_OF_THE_DEAD) {
        /* Prohibit Amulets in containers; if you allow it, monsters can't
         * steal them.  It also becomes a pain to check to see if someone
         * has the Amulet.  Ditto for the Candelabrum, the Bell and the Book.
         */
        pline("%s cannot be confined in such trappings.", The(xname(obj)));
        return 0;
    } else if (obj->otyp == LEASH && obj->leashmon != 0) {
        pline("%s attached to your pet.", Tobjnam(obj, "are"));
        return 0;
    } else if (obj == uwep) {
        if (welded(obj)) {
            weldmsg(obj);
            return 0;
        }
        setuwep((struct obj *) 0);
        /* This uwep check is obsolete.  It dates to 3.0 and earlier when
         * unwielding Firebrand would be fatal in hell if hero had no other
         * fire resistance.  Life-saving would force it to be re-wielded.
         */
        if (uwep)
            return 0; /* unwielded, died, rewielded */
    } else if (obj == uswapwep) {
        setuswapwep((struct obj *) 0);
    } else if (obj == uquiver) {
        setuqwep((struct obj *) 0);
    }

    if (fatal_corpse_mistake(obj, FALSE))
        return -1;

    /* boxes, boulders, and big statues can't fit into any container */
    if (obj->otyp == ICE_BOX || Is_box(obj) || obj->otyp == BOULDER
        || (obj->otyp == STATUE && bigmonst(&mons[obj->corpsenm]))) {
        /* consumes multiple obufs but not enough to overwrite the result */
        Strcpy(buf, the(xname(obj)));
        You("cannot fit %s into %s.", buf, the(xname(gc.current_container)));
        return 0;
    }

    freeinv(obj);

    if (obj_is_burning(obj)) /* this used to be part of freeinv() */
        (void) snuff_lit(obj);

    if (floor_container && costly_spot(u.ux, u.uy)) {
        /* defer gold until after put-in message */
        if (obj->oclass != COIN_CLASS) {
            /* sellobj() will take an unpaid item off the shop bill */
            was_unpaid = obj->unpaid ? TRUE : FALSE;
            if (gs.sellobj_first) {
                /* don't sell when putting the item into your own container,
                   but handle billing correctly */
                sellobj_state(gc.current_container->no_charge
                              ? SELL_DONTSELL : SELL_DELIBERATE);
                gs.sellobj_first = FALSE;
            }
            sellobj(obj, u.ux, u.uy);
        }
    }
    if (Icebox && !age_is_relative(obj)) {
        obj->age = svm.moves - obj->age; /* actual age */
        /* stop any corpse timeouts when frozen */
        if (obj->otyp == CORPSE) {
            if (obj->timed) {
                (void) stop_timer(ROT_CORPSE, obj_to_any(obj));
                (void) stop_timer(REVIVE_MON, obj_to_any(obj));
            }
            /* if this is the corpse of a cancelled ice troll, uncancel it */
            if (obj->corpsenm == PM_ICE_TROLL && has_omonst(obj))
                OMONST(obj)->mcan = 0;
        } else if (obj->globby && obj->timed) {
            (void) stop_timer(SHRINK_GLOB, obj_to_any(obj));
        }
    } else if (Is_mbag(gc.current_container) && mbag_explodes(obj, 0)) {
        livelog_printf(LL_ACHIEVE, "just blew up %s bag of holding", uhis());
        /* explicitly mention what item is triggering the explosion */
        urgent_pline(
              "As you put %s inside, you are blasted by a magical explosion!",
                     doname(obj));
        /* did not actually insert obj yet */
        if (was_unpaid)
            addtobill(obj, FALSE, FALSE, TRUE);
        if (obj->otyp == BAG_OF_HOLDING) /* one bag of holding into another */
            do_boh_explosion(obj, (boolean) (obj->where == OBJ_FLOOR));
        obfree(obj, (struct obj *) 0);
        /* if carried, shop goods will be flagged 'unpaid' and obfree() will
           handle bill issues, but if on floor, we need to put them on bill
           before deleting them (non-shop items will be flagged 'no_charge')*/
        if (floor_container && costly_spot(gc.current_container->ox,
                                           gc.current_container->oy)) {
            struct obj save_no_charge;

            save_no_charge.no_charge = gc.current_container->no_charge;
            addtobill(gc.current_container, FALSE, FALSE, FALSE);
            /* addtobill() clears no charge; we need to set it back
               so that useupf() doesn't double bill */
            gc.current_container->no_charge = save_no_charge.no_charge;
        }
        do_boh_explosion(gc.current_container, floor_container);

        if (!floor_container)
            useup(gc.current_container);
        else if (obj_here(gc.current_container, u.ux, u.uy))
            useupf(gc.current_container, gc.current_container->quan);
        else
            panic("in_container:  bag not found.");

        losehp(d(6, 6), "magical explosion", KILLED_BY_AN);
        gc.current_container = 0; /* baggone = TRUE; */
    }

    if (gc.current_container) {
        Strcpy(buf, the(xname(gc.current_container)));
        You("put %s into %s.", doname(obj), buf);

        /* gold in container always needs to be added to credit */
        if (floor_container && obj->oclass == COIN_CLASS)
            sellobj(obj, gc.current_container->ox, gc.current_container->oy);
        (void) add_to_container(gc.current_container, obj);
        gc.current_container->owt = weight(gc.current_container);
    }
    /* gold needs this, and freeinv() many lines above may cause
     * the encumbrance to disappear from the status, so just always
     * update status immediately.
     */
    bot();
    return (gc.current_container ? 1 : -1);
}

/* askchain() filter used by in_container();
 * returns True if the container is intact and 'obj' isn't it, False if
 * container is gone (magic bag explosion) or 'obj' is the container itself;
 * also used by getobj() when picking a single item to stash
 */
int
ck_bag(struct obj *obj)
{
    return (gc.current_container && obj != gc.current_container);
}

/* Returns: -1 to stop, 1 item was removed, 0 item was not removed. */
staticfn int
out_container(struct obj *obj)
{
    struct obj *otmp;
    int res;
    long count;
    boolean is_gold = (obj->oclass == COIN_CLASS);

    if (!gc.current_container) {
        impossible("<out> no gc.current_container?");
        return -1;
    } else if (is_gold) {
        obj->owt = weight(obj);
    }

    if (obj->oartifact && !touch_artifact(obj, &gy.youmonst))
        return 0;

    if (fatal_corpse_mistake(obj, FALSE))
        return -1;

    count = obj->quan;
    if ((res = lift_object(obj, gc.current_container, &count, FALSE)) <= 0)
        return res;

    if (obj->quan != count && obj->otyp != LOADSTONE)
        obj = splitobj(obj, count);

    /* Remove the object from the list. */
    obj_extract_self(obj);
    gc.current_container->owt = weight(gc.current_container);

    if (Icebox)
        removed_from_icebox(obj);

    if (!obj->unpaid && !carried(gc.current_container)
        && costly_spot(gc.current_container->ox, gc.current_container->oy)) {
        obj->ox = gc.current_container->ox;
        obj->oy = gc.current_container->oy;
        addtobill(obj, FALSE, FALSE, FALSE);
    }
    if (is_pick(obj))
        pick_pick(obj); /* shopkeeper feedback */

    otmp = addinv(obj);
    pickup_prinv(otmp, count, "removing");

    if (is_gold) {
        bot(); /* update character's gold piece count immediately */
    }
    return 1;
}

/* taking a corpse out of an ice box needs a couple of adjustments */
void
removed_from_icebox(struct obj *obj)
{
    if (!age_is_relative(obj)) {
        obj->age = svm.moves - obj->age; /* actual age */
        if (obj->otyp == CORPSE) {
            struct monst *m = get_mtraits(obj, FALSE);
            boolean iceT = m ? (m->data == &mons[PM_ICE_TROLL])
                             : (obj->corpsenm == PM_ICE_TROLL);

            /* start a revive timer if this corpse is for an ice troll,
               otherwise start a rot-away timer (even for other trolls) */
            obj->norevive = iceT ? 0 : 1;
            start_corpse_timeout(obj);
        } else if (obj->globby) {
            /* non-frozen globs gradually shrink away to nothing */
            start_glob_timeout(obj, 0L);
        }
    }
}

/* an object inside a cursed bag of holding is being destroyed */
staticfn long
mbag_item_gone(boolean held, struct obj *item, boolean silent)
{
    struct monst *shkp;
    long loss = 0L;

    if (!silent) {
        if (item->dknown)
            pline("%s %s vanished!", Doname2(item), otense(item, "have"));
        else
            You("%s %s disappear!", Blind ? "notice" : "see", doname(item));
    }

    if (*u.ushops && (shkp = shop_keeper(*u.ushops)) != 0) {
        if (held ? (boolean) item->unpaid : costly_spot(u.ux, u.uy))
            loss = stolen_value(item, u.ux, u.uy, (boolean) shkp->mpeaceful,
                                TRUE);
    }
    obfree(item, (struct obj *) 0);
    return loss;
}

/* used for #loot/apply, #tip, and final disclosure */
void
observe_quantum_cat(struct obj *box, boolean makecat, boolean givemsg)
{
    static NEARDATA const char sc[] = "Schroedinger's Cat";
    struct obj *deadcat;
    struct monst *livecat = 0;
    coordxy ox, oy;
    boolean itsalive = !rn2(2);

    if (get_obj_location(box, &ox, &oy, 0))
        box->ox = ox, box->oy = oy; /* in case it's being carried */

    /* this isn't really right, since any form of observation
       (telepathic or monster/object/food detection) ought to
       force the determination of alive vs dead state; but basing it
       just on opening or disclosing the box is much simpler to cope with */

    /* SchroedingersBox already has a cat corpse in it */
    deadcat = box->cobj;
    if (itsalive) {
        if (makecat)
            livecat = makemon(&mons[PM_HOUSECAT], box->ox, box->oy,
                              NO_MINVENT | MM_ADJACENTOK | MM_NOMSG);
        if (livecat) {
            livecat->mpeaceful = 1;
            set_malign(livecat);
            if (givemsg) {
                if (!canspotmon(livecat))
                    You("think %s brushed your %s.", something,
                        body_part(FOOT));
                else
                    pline("%s inside the box is still alive!",
                          Monnam(livecat));
            }
            (void) christen_monst(livecat, sc);
            if (deadcat) {
                obj_extract_self(deadcat);
                obfree(deadcat, (struct obj *) 0), deadcat = 0;
            }
            box->owt = weight(box);
            box->spe = 0;

            if (!svc.context.mon_moving) {
                /* give experience points for releasing live cat; slightly
                   different amount from what is given for "killing" it */
                more_experienced(10, 20); /* 10:current exp; 20:score bonus */
                newexplevel();
            }
        }
    } else {
        box->spe = 0; /* now an ordinary box (with a cat corpse inside) */
        if (givemsg)
            pline_The("%s inside the box is dead!",
                      Hallucination ? rndmonnam((char *) 0) : "housecat");
        if (deadcat) {
            /* set_corpsenm() will start the rot timer that was removed
               when makemon() created SchroedingersBox; start it from
               now rather than from when this special corpse got created */
            deadcat->age = svm.moves;
            set_corpsenm(deadcat, PM_HOUSECAT);
            deadcat = oname(deadcat, sc, ONAME_NO_FLAGS);

            if (!svc.context.mon_moving) {
                /* give experience points for the death of the cat since
                   that has been finalized by the hero opening the box */
                more_experienced(20, 10); /* 20:current exp; 10:score bonus */
                newexplevel();
            }
        }
    }
    nhUse(deadcat);
    return;
}

#undef Icebox

/* used by askchain() to check for magic bag explosion */
boolean
container_gone(int (*fn)(OBJ_P))
{
    /* result is only meaningful while use_container() is executing */
    return ((fn == in_container || fn == out_container)
            && !gc.current_container);
}

staticfn void
explain_container_prompt(boolean more_containers)
{
    static const char *const explaintext[] = {
        "Container actions:",
        "",
        " : -- Look: examine contents",
        " o -- Out: take things out",
        " i -- In: put things in",
        " b -- Both: first take things out, then put things in",
        " r -- Reversed: put things in, then take things out",
        " s -- Stash: put one item in", "",
        " n -- Next: loot next selected container",
        " q -- Quit: finished",
        " ? -- Help: display this text.",
        "", 0
    };
    const char *const *txtpp;
    winid win;

    /* "Do what with <container>? [:oibrsq or ?] (q)" */
    if ((win = create_nhwindow(NHW_TEXT)) != WIN_ERR) {
        for (txtpp = explaintext; *txtpp; ++txtpp) {
            if (!more_containers && !strncmp(*txtpp, " n ", 3))
                continue;
            putstr(win, 0, *txtpp);
        }
        display_nhwindow(win, FALSE);
        destroy_nhwindow(win);
    }
}

boolean
u_handsy(void)
{
    if (nohands(gy.youmonst.data)) {
        You("have no hands!"); /* not `body_part(HAND)' */
        return FALSE;
    } else if (!freehand()) {
        You("have no free %s.", body_part(HAND));
        return FALSE;
    }
    return TRUE;
}

/* getobj callback for object to be stashed into a container */
staticfn int
stash_ok(struct obj *obj)
{
    if (!obj)
        return GETOBJ_EXCLUDE;

    /* downplay the container being stashed into */
    if (!ck_bag(obj))
        return GETOBJ_EXCLUDE_SELECTABLE;
    /* Possible extension: downplay things too big to fit into containers (in
     * which case extract in_container()'s logic.) */

    return GETOBJ_SUGGEST;
}

int
use_container(
    struct obj **objp,
    boolean held,
    boolean more_containers) /* True iff #loot multiple and this isn't last */
{
    struct obj *otmp, *obj = *objp;
    boolean quantum_cat, cursed_mbag, loot_out, loot_in, loot_in_first,
        stash_one, inokay, outokay, outmaybe;
    char c, emptymsg[BUFSZ], qbuf[QBUFSZ], pbuf[QBUFSZ], xbuf[QBUFSZ];
    int used = ECMD_OK;
    long loss;

    ga.abort_looting = FALSE;
    gs.sellobj_first = TRUE; /* in_container() should call sellobj_state() */
    emptymsg[0] = '\0';

    if (!u_handsy())
        return ECMD_OK;

    if (!obj->lknown) { /* do this in advance */
        obj->lknown = 1;
        if (held)
            update_inventory();
    }
    if (obj->olocked) {
        pline("%s locked.", Tobjnam(obj, "are"));
        if (held)
            You("must put it down to unlock.");
        return ECMD_OK;
    } else if (obj->otrapped) {
        if (held)
            You("open %s...", the(xname(obj)));
        (void) chest_trap(obj, HAND, FALSE);
        /* even if the trap fails, you've used up this turn */
        if (gm.multi >= 0) { /* in case we didn't become paralyzed */
            nomul(-1);
            gm.multi_reason = "opening a container";
            gn.nomovemsg = "";
        }
        ga.abort_looting = TRUE;
        return ECMD_TIME;
    }

    gc.current_container = obj; /* for use by in/out_container */
    /*
     * From here on out, all early returns go through 'containerdone:'.
     */

    /* check for Schroedinger's Cat */
    quantum_cat = SchroedingersBox(gc.current_container);
    if (quantum_cat) {
        observe_quantum_cat(gc.current_container, TRUE, TRUE);
        used = ECMD_TIME;
    }

    cursed_mbag = Is_mbag(gc.current_container)
        && gc.current_container->cursed
        && Has_contents(gc.current_container);
    if (cursed_mbag
        && (loss = boh_loss(gc.current_container, held)) != 0) {
        used = ECMD_TIME;
        You("owe %ld %s for lost merchandise.", loss, currency(loss));
        gc.current_container->owt = weight(gc.current_container);
    }
    /* might put something in if carrying anything other than just the
       container itself (invent is not the container or has a next object) */
    inokay = (gi.invent != 0 && (gi.invent != gc.current_container
                                || gi.invent->nobj));
    /* might take something out if container isn't empty */
    outokay = Has_contents(gc.current_container);
    if (!outokay) /* preformat the empty-container message */
        Sprintf(emptymsg, "%s is %sempty.",
                Ysimple_name2(gc.current_container),
                (quantum_cat || cursed_mbag) ? "now " : "");

    /*
     * What-to-do prompt's list of possible actions:
     * always include the look-inside choice (':');
     * include the take-out choice ('o') if container
     * has anything in it or if player doesn't yet know
     * that it's empty (latter can change on subsequent
     * iterations if player picks ':' response);
     * include the put-in choices ('i','s') if hero
     * carries any inventory (including gold) aside from
     * the container itself;
     * include do-both when 'o' is available, even if
     * inventory is empty--taking out could alter that;
     * include do-both-reversed when 'i' is available,
     * even if container is empty--for similar reason;
     * include the next container choice ('n') when
     * relevant, and make it the default;
     * always include the quit choice ('q'), and make
     * it the default if there's no next container;
     * include the help choice (" or ?") if `cmdassist'
     * run-time option is set;
     * (Player can pick any of (o,i,b,r,n,s,?) even when
     * they're not listed among the available actions.)
     *
     * Do what with <the/your/Shk's container>? [:oibrs nq or ?] (q)
     * or
     * <The/Your/Shk's container> is empty.  Do what with it? [:irs nq or ?]
     */
    for (;;) { /* repeats iff '?' or ':' gets chosen */
        outmaybe = (outokay || !gc.current_container->cknown);
        if (!outmaybe)
            (void) safe_qbuf(qbuf, (char *) 0, " is empty.  Do what with it?",
                             gc.current_container, Yname2, Ysimple_name2,
                             "This");
        else
            (void) safe_qbuf(qbuf, "Do what with ", "?", gc.current_container,
                             yname, ysimple_name, "it");
        /* ask player about what to do with this container */
        if (flags.menu_style == MENU_PARTIAL
            || flags.menu_style == MENU_FULL) {
            if (!inokay && !outmaybe) {
                /* nothing to take out, nothing to put in;
                   trying to do both will yield proper feedback */
                c = 'b';
            } else {
                c = in_or_out_menu(qbuf, gc.current_container,
                                   outmaybe, inokay,
                                   (boolean) (used != ECMD_OK),
                                   more_containers);
            }
        } else { /* TRADITIONAL or COMBINATION */
            xbuf[0] = '\0'; /* list of extra acceptable responses */
            Strcpy(pbuf, ":");                   /* look inside */
            Strcat(outmaybe ? pbuf : xbuf, "o"); /* take out */
            Strcat(inokay ? pbuf : xbuf, "i");   /* put in */
            Strcat(outmaybe ? pbuf : xbuf, "b"); /* both */
            Strcat(inokay ? pbuf : xbuf, "rs");  /* reversed, stash */
            Strcat(pbuf, " ");                   /* separator */
            Strcat(more_containers ? pbuf : xbuf, "n"); /* next container */
            Strcat(pbuf, "q");                   /* quit */
            if (iflags.cmdassist)
                /* this unintentionally allows user to answer with 'o' or
                   'r'; fortunately, those are already valid choices here */
                Strcat(pbuf, " or ?"); /* help */
            else
                Strcat(xbuf, "?");
            if (*xbuf)
                Strcat(strcat(pbuf, "\033"), xbuf);
            c = yn_function(qbuf, pbuf, more_containers ? 'n' : 'q', TRUE);
        } /* PARTIAL|FULL vs other modes */

        if (c == '?') {
            explain_container_prompt(more_containers);
        } else if (c == ':') { /* note: will set obj->cknown */
            if (!gc.current_container->cknown)
                used = ECMD_TIME; /* gaining info */
            container_contents(gc.current_container, FALSE, FALSE, TRUE);
        } else
            break;
    } /* loop until something other than '?' or ':' is picked */

    if (c == 'q')
        ga.abort_looting = TRUE;
    if (c == 'n' || c == 'q') /* [not strictly needed; falling thru works] */
        goto containerdone;
    loot_out = (c == 'o' || c == 'b' || c == 'r');
    loot_in = (c == 'i' || c == 'b' || c == 'r');
    loot_in_first = (c == 'r'); /* both, reversed */
    stash_one = (c == 's');

    /* out-only or out before in */
    if (loot_out && !loot_in_first) {
        if (!Has_contents(gc.current_container)) {
            pline1(emptymsg); /* <whatever> is empty. */
            if (!gc.current_container->cknown)
                used = ECMD_TIME;
            gc.current_container->cknown = 1;
        } else {
            add_valid_menu_class(0); /* reset */
            if (flags.menu_style == MENU_TRADITIONAL)
                used |= traditional_loot(FALSE);
            else
                used |= (menu_loot(0, FALSE) > 0);
            add_valid_menu_class(0);
        }
        /* recalculate 'inokay' in case something was just taken out and
           inventory is no longer empty or no longer just the container */
        inokay = (gi.invent && (gi.invent != gc.current_container
                               || gi.invent->nobj));
    }

    if ((loot_in || stash_one) && !inokay) {
        You("don't have anything%s to %s.", gi.invent ? " else" : "",
            stash_one ? "stash" : "put in");
        loot_in = stash_one = FALSE;
    }

    /*
     * Gone: being nice about only selecting food if we know we are
     * putting things in an ice chest.
     */
    if (loot_in) {
        add_valid_menu_class(0); /* reset */
        if (flags.menu_style == MENU_TRADITIONAL)
            used |= traditional_loot(TRUE);
        else
            used |= (menu_loot(0, TRUE) > 0);
        add_valid_menu_class(0);
    } else if (stash_one) {
        /* put one item into container */
        if ((otmp = getobj("stash", stash_ok,
                           GETOBJ_PROMPT | GETOBJ_ALLOWCNT)) != 0) {
            if (in_container(otmp)) {
                used = 1;
            } else {
                /* couldn't put selected item into container for some
                   reason; might need to undo splitobj() */
                (void) unsplitobj(otmp);
            }
        }
    }
    /* putting something in might have triggered magic bag explosion */
    if (!gc.current_container)
        loot_out = FALSE;

    /* out after in */
    if (loot_out && loot_in_first) {
        if (!Has_contents(gc.current_container)) {
            pline1(emptymsg); /* <whatever> is empty. */
            if (!gc.current_container->cknown)
                used = 1;
            gc.current_container->cknown = 1;
        } else {
            add_valid_menu_class(0); /* reset */
            if (flags.menu_style == MENU_TRADITIONAL)
                used |= traditional_loot(FALSE);
            else
                used |= (menu_loot(0, FALSE) > 0);
            add_valid_menu_class(0);
        }
    }

 containerdone:
    if (used) {
        /* Not completely correct; if we put something in without knowing
           whatever was already inside, now we suddenly do.  That can't
           be helped unless we want to track things item by item and then
           deal with containers whose contents are "partly known". */
        if (gc.current_container)
            gc.current_container->cknown = 1;
        update_inventory();
    }

    sellobj_state(SELL_NORMAL); /* in case in_container() set it */
    *objp = gc.current_container; /* might have become null */
    if (gc.current_container)
        gc.current_container = 0; /* avoid hanging on to stale pointer */
    else
        ga.abort_looting = TRUE;
    return used;
}

/* loot current_container (take things out or put things in), by prompting */
staticfn int
traditional_loot(boolean put_in)
{
    int (*actionfunc)(OBJ_P), (*checkfunc)(OBJ_P);
    struct obj **objlist;
    char selection[MAXOCLASSES + 10]; /* +10: room for B,U,C,X plus slop */
    const char *action;
    boolean one_by_one, allflag;
    int used = ECMD_OK, menu_on_request = 0;

    if (put_in) {
        action = "put in";
        objlist = &gi.invent;
        actionfunc = in_container;
        checkfunc = ck_bag;
    } else {
        action = "take out";
        objlist = &(gc.current_container->cobj);
        actionfunc = out_container;
        checkfunc = (int (*)(OBJ_P)) 0;
        gp.pickup_encumbrance = 0; /* used to limit verbosity */
    }

    if (query_classes(selection, &one_by_one, &allflag, action, *objlist,
                      FALSE, &menu_on_request)) {
        if (askchain(objlist, (one_by_one ? (char *) 0 : selection), allflag,
                     actionfunc, checkfunc, 0, action))
            used = ECMD_TIME;
    } else if (menu_on_request < 0) {
        used = (menu_loot(menu_on_request, put_in) > 0);
    }
    return used;
}

/* loot current_container (take things out or put things in), using a menu */
staticfn int
menu_loot(int retry, boolean put_in)
{
    int n, i, n_looted = 0;
    boolean all_categories = TRUE, loot_everything = FALSE, autopick = FALSE;
    char buf[BUFSZ];
    boolean loot_justpicked = FALSE;
    const char *action = put_in ? "Put in" : "Take out";
    struct obj *otmp, *otmp2;
    menu_item *pick_list;
    int mflags, res;
    long count = 0;

    gp.pickup_encumbrance = 0; /* used by out_container(); no harm in
                                * zeroing it if about to use in_container() */
    if (retry) {
        all_categories = (retry == -2);
    } else if (flags.menu_style == MENU_FULL) {
        all_categories = FALSE;
        Sprintf(buf, "%s what type of objects?", action);
        mflags = (ALL_TYPES | UNPAID_TYPES | BUCX_TYPES | CHOOSE_ALL
                  | JUSTPICKED );
        n = query_category(buf,
                           put_in ? gi.invent : gc.current_container->cobj,
                           mflags, &pick_list, PICK_ANY);
            /* when paranoid_confirm:A is set, 'A' by itself implies
               'A'+'a' which will be followed by a confirmation prompt;
               when that option isn't set, 'A' by itself is rejected
               by query_categorry() and result here will be n==0 */
        if (!n)
            return ECMD_OK; /* no non-autopick category filters specified */

        for (i = 0; i < n; i++) {
            if (pick_list[i].item.a_int == 'A') {
                loot_everything = autopick = TRUE;
            } else if (put_in && pick_list[i].item.a_int == 'P') {
                loot_justpicked = TRUE;
                count = max(0, pick_list[i].count);
                add_valid_menu_class(pick_list[i].item.a_int);
                loot_everything = FALSE;
            } else if (pick_list[i].item.a_int == ALL_TYPES_SELECTED) {
                all_categories = TRUE;
            } else {
                add_valid_menu_class(pick_list[i].item.a_int);
                loot_everything = FALSE;
            }
        }
        free((genericptr_t) pick_list);
    }

    if (autopick) {
        int (*inout_func)(struct obj *); /* in_container or out_container */
        struct obj *firstobj;

        if (!put_in) {
            gc.current_container->cknown = 1;
            inout_func = out_container;
            firstobj = gc.current_container->cobj;
        } else {
            inout_func = in_container;
            firstobj = gi.invent;
        }
        /*
         * Note:  for put_in, current_container might be destroyed during
         * mid-traversal by a magic bag explosion.
         * Note too:  items are processed in internal list order rather
         * than menu display order ('sortpack') or 'sortloot' order;
         * for put_in that should be item->invlet order so reasonable.
         */
        for (otmp = firstobj; otmp && gc.current_container; otmp = otmp2) {
            otmp2 = otmp->nobj;
            if (loot_everything || all_categories || allow_category(otmp)) {
                res = (*inout_func)(otmp);
                if (res < 0)
                    break;
                n_looted += res;
            }
        }
    } else if (put_in && loot_justpicked
               && count_justpicked(gi.invent) == 1) {
        otmp = find_justpicked(gi.invent);
        if (otmp) {
            n_looted = 1;
            if (count > 0 && count < otmp->quan) {
                otmp = splitobj(otmp, count);
            }
            (void) in_container(otmp);
            /* return value doesn't matter, even if container blew up */
        }
    } else {
        mflags = INVORDER_SORT | INCLUDE_VENOM;
        if (put_in && flags.invlet_constant)
            mflags |= USE_INVLET;
        if (put_in && loot_justpicked)
            mflags |= JUSTPICKED;
        if (!put_in)
            gc.current_container->cknown = 1;
        Sprintf(buf, "%s what?", action);
        n = query_objlist(buf,
                          put_in ? &gi.invent : &(gc.current_container->cobj),
                          mflags, &pick_list, PICK_ANY,
                          all_categories ? allow_all : allow_category);
        if (n) {
            n_looted = n;
            for (i = 0; i < n; i++) {
                otmp = pick_list[i].item.a_obj;
                assert(otmp != 0);
                count = pick_list[i].count;
                if (count > 0 && count < otmp->quan) {
                    otmp = splitobj(otmp, count);
                    /* special split case also handled by askchain() */
                }
                res = put_in ? in_container(otmp) : out_container(otmp);
                if (res <= 0) {
                    if (!gc.current_container) {
                        /* otmp caused current_container to explode;
                           both are now gone */
                        otmp = 0; /* and break loop */
                    } else if (otmp && otmp != pick_list[i].item.a_obj) {
                        /* split occurred, merge again */
                        (void) unsplitobj(otmp);
                    }
                    if (res < 0)
                        break;
                }
            }
            free((genericptr_t) pick_list);
        }
    }
    return n_looted ? ECMD_TIME : ECMD_OK;
}

staticfn char
in_or_out_menu(
    const char *prompt,
    struct obj *obj,
    boolean outokay,     /* can take out */
    boolean inokay,      /* can put in */
    boolean alreadyused, /* controls phrasing of the decline choice */
    boolean more_containers)
{
    /* underscore is not a choice; it's used to skip element [0] */
    static const char lootchars[] = "_:oibrsnq", abc_chars[] = "_:abcdenq";
    winid win;
    anything any;
    menu_item *pick_list;
    char buf[BUFSZ];
    int n;
    const char *menuselector = flags.lootabc ? abc_chars : lootchars;
    int clr = NO_COLOR;

    any = cg.zeroany;
    win = create_nhwindow(NHW_MENU);
    start_menu(win, MENU_BEHAVE_STANDARD);

    any.a_int = 1; /* ':' */
    Sprintf(buf, "Look inside %s", thesimpleoname(obj));
    add_menu(win, &nul_glyphinfo, &any, menuselector[any.a_int], 0,
             ATR_NONE, clr, buf, MENU_ITEMFLAGS_NONE);
    if (outokay) {
        any.a_int = 2; /* 'o' */
        Sprintf(buf, "take %s out", something);
        add_menu(win, &nul_glyphinfo, &any, menuselector[any.a_int], 0,
                 ATR_NONE, clr, buf, MENU_ITEMFLAGS_NONE);
    }
    if (inokay) {
        any.a_int = 3; /* 'i' */
        Sprintf(buf, "put %s in", something);
        add_menu(win, &nul_glyphinfo, &any, menuselector[any.a_int], 0,
                 ATR_NONE, clr, buf, MENU_ITEMFLAGS_NONE);
    }
    if (outokay) {
        any.a_int = 4; /* 'b' */
        Sprintf(buf, "%stake out, then put in", inokay ? "both; " : "");
        add_menu(win, &nul_glyphinfo, &any, menuselector[any.a_int], 0,
                 ATR_NONE, clr, buf, MENU_ITEMFLAGS_NONE);
    }
    if (inokay) {
        any.a_int = 5; /* 'r' */
        Sprintf(buf, "%sput in, then take out",
                outokay ? "both reversed; " : "");
        add_menu(win, &nul_glyphinfo, &any, menuselector[any.a_int], 0,
                 ATR_NONE, clr, buf, MENU_ITEMFLAGS_NONE);
        any.a_int = 6; /* 's' */
        Sprintf(buf, "stash one item into %s", thesimpleoname(obj));
        add_menu(win, &nul_glyphinfo, &any, menuselector[any.a_int], 0,
                 ATR_NONE, clr, buf, MENU_ITEMFLAGS_NONE);
    }
    add_menu_str(win, "");
    if (more_containers) {
        any.a_int = 7; /* 'n' */
        add_menu(win, &nul_glyphinfo, &any, menuselector[any.a_int], 0,
                 ATR_NONE, clr, "loot next container",
                 MENU_ITEMFLAGS_SELECTED);
    }
    any.a_int = 8; /* 'q' */
    Strcpy(buf, alreadyused ? "done" : "do nothing");
    add_menu(win, &nul_glyphinfo, &any, menuselector[any.a_int], 0,
             ATR_NONE, clr, buf,
             more_containers ? MENU_ITEMFLAGS_NONE : MENU_ITEMFLAGS_SELECTED);

    end_menu(win, prompt);
    n = select_menu(win, PICK_ONE, &pick_list);
    destroy_nhwindow(win);
    if (n > 0) {
        int k = pick_list[0].item.a_int;

        if (n > 1 && k == (more_containers ? 7 : 8))
            k = pick_list[1].item.a_int;
        free((genericptr_t) pick_list);
        return lootchars[k]; /* :,o,i,b,r,s,n,q */
    }
    return (n == 0 && more_containers) ? 'n' : 'q'; /* next or quit */
}

/* getobj callback for object to tip */
staticfn int
tip_ok(struct obj *obj)
{
    if (!obj || obj->oclass == COIN_CLASS)
        return GETOBJ_EXCLUDE;

    if (Is_container(obj)) {
        return GETOBJ_SUGGEST;
    }

    /* include horn of plenty if sufficiently discovered */
    if (obj->otyp == HORN_OF_PLENTY && obj->dknown
        && objects[obj->otyp].oc_name_known)
        return GETOBJ_SUGGEST;

    /* allow trying anything else in inventory */
    return GETOBJ_DOWNPLAY;
}

/* show a menu of containers under hero,
   and one extra entry for choosing an inventory.
   returns ECMD_CANCEL if menu was canceled,
   ECMD_TIME if a container was picked,
   otherwise returns ECMD_OK. */
staticfn int
choose_tip_container_menu(void)
{
    int n, i;
    winid win;
    anything any;
    menu_item *pick_list = (menu_item *) 0;
    struct obj dummyobj, *otmp;
    int clr = NO_COLOR;

    any = cg.zeroany;
    win = create_nhwindow(NHW_MENU);
    start_menu(win, MENU_BEHAVE_STANDARD);

    for (otmp = svl.level.objects[u.ux][u.uy], i = 0; otmp;
         otmp = otmp->nexthere)
        if (Is_container(otmp)) {
            ++i;
            any.a_obj = otmp;
            add_menu(win, &nul_glyphinfo, &any, 0, 0, ATR_NONE,
                     clr, doname(otmp), MENU_ITEMFLAGS_NONE);
        }
    if (gi.invent) {
        add_menu_str(win, "");
        any.a_obj = &dummyobj;
        /* use 'i' for inventory unless there are so many
           containers that it's already being used */
        i = (i <= 'i' - 'a' && !flags.lootabc) ? 'i' : 0;
        add_menu(win, &nul_glyphinfo, &any, i, 0, ATR_NONE,
                 clr, "tip something being carried",
                 MENU_ITEMFLAGS_SELECTED);
    }
    end_menu(win, "Tip which container?");
    n = select_menu(win, PICK_ONE, &pick_list);
    destroy_nhwindow(win);
    /*
     * Deal with quirk of preselected item in pick-one menu:
     * n ==  0 => picked preselected entry, toggling it off;
     * n ==  1 => accepted preselected choice via SPACE or RETURN;
     * n ==  2 => picked something other than preselected entry;
     * n == -1 => cancelled via ESC;
     */
    otmp = (n <= 0) ? (struct obj *) 0 : pick_list[0].item.a_obj;
    if (n > 1 && otmp == &dummyobj)
        otmp = pick_list[1].item.a_obj;
    if (pick_list)
        free((genericptr_t) pick_list);
    if (otmp && otmp != &dummyobj) {
        tipcontainer(otmp);
        return ECMD_TIME;
    }
    if (n == -1)
        return ECMD_CANCEL;
    return ECMD_OK;
}

/* #tip command -- empty container contents onto floor */
int
dotip(void)
{
    struct obj *cobj, *nobj;
    coord cc;
    int boxes;
    char c, buf[BUFSZ], qbuf[BUFSZ];
    const char *spillage = 0;

    /*
     * Doesn't require free hands;
     * limbs are needed to tip floor containers.
     *
     * Note: for menustyle:Traditional, using m prefix forces a menu
     * of floor containers when more than one is present.  For other
     * menustyle settings or when fewer than two floor containers are
     * present, using 'm' skips floor and goes straight to invent.
     * This somewhat unintuitive behavior is driven by the way that
     * context-sensitive inventory item actions use m prefix.
     */

    /* at present, can only tip things at current spot, not adjacent ones */
    cc.x = u.ux, cc.y = u.uy;

    /* check floor container(s) first; at most one will be accessed */
    boxes = container_at(cc.x, cc.y, TRUE);
    /* this is iffy for menustyle:traditional; 'm' prefix is ambiguous
       for it: skip floor vs handle multiple containers via menu */
    if (boxes > 0
        && (!iflags.menu_requested
            || (flags.menu_style == MENU_TRADITIONAL && boxes > 1))) {
        Sprintf(buf, "You can't tip %s while carrying so much.",
                !flags.verbose ? "a container" : (boxes > 1) ? "one" : "it");
        if (!check_capacity(buf) && able_to_loot(cc.x, cc.y, FALSE)) {
            if (boxes > 1) {
                int res;
                /* pick one container via menu or ... */
                if ((res = choose_tip_container_menu()) != ECMD_OK)
                    return res;
                /* else pick-from-gi.invent below */
            } else {
                for (cobj = svl.level.objects[cc.x][cc.y]; cobj;
                     cobj = nobj) {
                    nobj = cobj->nexthere;
                    if (!Is_container(cobj))
                        continue;
                    c = ynq(safe_qbuf(qbuf, "There is ", " here, tip it?",
                                      cobj,
                                      doname, ansimpleoname, "container"));
                    if (c == 'q')
                        return ECMD_OK;
                    if (c == 'n')
                        continue;
                    tipcontainer(cobj);
                    /* can only tip one container at a time */
                    return ECMD_TIME;
                }
            }
        }
    }

    /* either no floor container(s) or 'm' prefix was used to ignore such
       or couldn't tip one or didn't tip any */
    cobj = getobj("tip", tip_ok, GETOBJ_PROMPT);
    if (!cobj)
        return ECMD_CANCEL;

    /* normal case */
    if (Is_container(cobj) || cobj->otyp == HORN_OF_PLENTY) {
        tipcontainer(cobj);
        return ECMD_TIME;
    }
    /* assorted other cases */
    if (Is_candle(cobj) && cobj->lamplit) {
        /* note "wax" even for tallow candles to avoid giving away info */
        spillage = "wax";
    } else if ((cobj->otyp == POT_OIL && cobj->lamplit)
               || (cobj->otyp == OIL_LAMP && cobj->age != 0L)
               || (cobj->otyp == MAGIC_LAMP && cobj->spe != 0)) {
        spillage = "oil";
        /* todo: reduce potion's remaining burn timer or oil lamp's fuel */
    } else if (cobj->otyp == CAN_OF_GREASE && cobj->spe > 0) {
        /* charged consumed below */
        spillage = "grease";
    } else if (cobj->otyp == FOOD_RATION || cobj->otyp == CRAM_RATION
               || cobj->otyp == LEMBAS_WAFER) {
        spillage = "crumbs";
    } else if (cobj->oclass == VENOM_CLASS) {
        spillage = "venom";
    }
    if (spillage) {
        buf[0] = '\0';
        if (is_pool(u.ux, u.uy))
            Sprintf(buf, " and gradually %s", vtense(spillage, "dissipate"));
        else if (is_lava(u.ux, u.uy))
            Sprintf(buf, " and immediately %s away",
                    vtense(spillage, "burn"));
        pline("Some %s %s onto the %s%s.", spillage,
              vtense(spillage, "spill"), surface(u.ux, u.uy), buf);
        /* shop usage message comes after the spill message */
        if (cobj->otyp == CAN_OF_GREASE && cobj->spe > 0) {
            consume_obj_charge(cobj, TRUE);
        }
        /* something [useless] happened */
        return ECMD_TIME;
    }
    /* anything not covered yet */
    if (cobj->oclass == POTION_CLASS) /* can't pour potions... */
        pline_The("%s %s securely sealed.", xname(cobj), otense(cobj, "are"));
    else if (uarmh && cobj == uarmh)
        return tiphat() ? ECMD_TIME : ECMD_OK;
    else if (cobj->otyp == STATUE)
        pline("Nothing interesting happens.");
    else
        pline1(nothing_happens);
    return ECMD_OK;
}

enum tipping_check_values {
    TIPCHECK_OK = 0,
    TIPCHECK_LOCKED,
    TIPCHECK_TRAPPED,
    TIPCHECK_CANNOT,
    TIPCHECK_EMPTY
};

staticfn void
tipcontainer(struct obj *box) /* or bag */
{
    coordxy ox = u.ux, oy = u.uy; /* #tip only works at hero's location */
    boolean srcheld = FALSE, dstheld = FALSE, maybeshopgoods;
    struct obj *targetbox = (struct obj *) 0;
    boolean cancelled = FALSE;

    /* box is either held or on floor at hero's spot; no need to check for
       nesting; when held, we need to update its location to match hero's;
       for floor, the coordinate updating is redundant */
    if (get_obj_location(box, &ox, &oy, 0))
        box->ox = ox, box->oy = oy;

    /*
     * TODO?
     *  if 'box' is known to be empty or known to be locked, give up
     *  before choosing 'targetbox'.
     */
    targetbox = tipcontainer_gettarget(box, &cancelled);
    if (cancelled)
        return;

    /* Shop handling:  can't rely on the container's own unpaid
       or no_charge status because contents might differ with it.
       A carried container's contents will be flagged as unpaid
       or not, as appropriate, and need no special handling here.
       Items owned by the hero get sold to the shop without
       confirmation as with other uncontrolled drops.  A floor
       container's contents will be marked no_charge if owned by
       hero, otherwise they're owned by the shop.  By passing
       the contents through shop billing, they end up getting
       treated the same as in the carried case.   We do so one
       item at a time instead of doing whole container at once
       to reduce the chance of exhausting shk's billing capacity. */
    maybeshopgoods = !carried(box) && costly_spot(box->ox, box->oy);

    if (tipcontainer_checks(box, targetbox, FALSE) != TIPCHECK_OK)
        return;
    if (targetbox
        && tipcontainer_checks(targetbox, NULL, TRUE) != TIPCHECK_OK)
        return;

    {
        struct obj *otmp, *nobj;
        boolean terse, highdrop = !can_reach_floor(TRUE),
                altarizing = IS_ALTAR(levl[ox][oy].typ),
                cursed_mbag = (Is_mbag(box) && box->cursed);
        long loss = 0L;

        srcheld = carried(box);
        dstheld = (targetbox && carried(targetbox));
        if (u.uswallow)
            highdrop = altarizing = FALSE;
        terse = !(highdrop || altarizing || costly_spot(box->ox, box->oy));
        box->cknown = 1;
        /* Terse formatting is
         * "Objects spill out: obj1, obj2, obj3, ..., objN."
         * If any other messages intervene between objects, we revert to
         * "ObjK drops to the floor.", "ObjL drops to the floor.", &c.
         */
        if (targetbox)
            pline("%s into %s.",
                  box->cobj->nobj ? "Objects tumble" : "An object tumbles",
                  the(xname(targetbox)));
        else
            pline("%s out%c",
              box->cobj->nobj ? "Objects spill" : "An object spills",
              terse ? ':' : '.');

        for (otmp = box->cobj; otmp; otmp = nobj) {
            nobj = otmp->nobj;
            obj_extract_self(otmp);
            otmp->ox = box->ox, otmp->oy = box->oy;

            if (box->otyp == ICE_BOX) {
                removed_from_icebox(otmp); /* resume rotting for corpse */
            } else if (cursed_mbag && is_boh_item_gone()) {
                loss += mbag_item_gone(srcheld, otmp, FALSE);
                /* abbreviated drop format is no longer appropriate */
                terse = FALSE;
                continue;
            }
            if (maybeshopgoods) {
                addtobill(otmp, FALSE, FALSE, TRUE);
                iflags.suppress_price++; /* doname formatting */
            }

            if (targetbox) {
                if (Is_mbag(targetbox) && mbag_explodes(otmp, 0)) {
                    livelog_printf(LL_ACHIEVE,
                                 "just blew up %s bag of holding via tipping",
                                   uhis());
                    /* explicitly mention what item is triggering explosion */
                    urgent_pline(
                   "As %s %s inside, you are blasted by a magical explosion!",
                                 doname(otmp), otense(otmp, "tumble"));

                    /* if putting one bag of holding into another, first
                       blow up the one going in, then (below) blow up the
                       one it's going into */
                    if (otmp->otyp == BAG_OF_HOLDING) /* BoH into another */
                        do_boh_explosion(otmp, !srcheld);
                    /* always delete the item which triggered the explosion */
                    obfree(otmp, (struct obj *) 0); /* where==OBJ_FREE */

                    /* [assumes targetbox is carried, otherwise shop bill
                       handling becomes necessary here] */
                    do_boh_explosion(targetbox, !dstheld);
                    if (dstheld)
                        useup(targetbox);
                    else
                        useupf(targetbox, targetbox->quan);
                    targetbox = 0; /* it's gone */
                    nobj = 0; /* stop tipping; want loop to exit 'normally' */

                    losehp(d(6, 6), "magical explosion", KILLED_BY_AN);
                } else {
                    (void) add_to_container(targetbox, otmp);
                }
            } else if (highdrop) {
                otmp->how_lost = LOST_DROPPED;
                /* might break or fall down stairs; handles altars itself */
                hitfloor(otmp, TRUE);
            } else {
                if (altarizing) {
                    doaltarobj(otmp);
                } else if (!terse) {
                    pline("%s %s to the %s.", Doname2(otmp),
                          otense(otmp, "drop"), surface(ox, oy));
                } else {
                    pline("%s%c", doname(otmp), nobj ? ',' : '.');
                    iflags.last_msg = PLNMSG_OBJNAM_ONLY;
                }
                otmp->how_lost = LOST_DROPPED;
                dropy(otmp);
                if (iflags.last_msg != PLNMSG_OBJNAM_ONLY)
                    terse = FALSE; /* terse formatting has been interrupted */
            }

            if (maybeshopgoods)
                iflags.suppress_price--; /* reset */
        }
        if (loss) /* magic bag lost some shop goods */
            You("owe %ld %s for lost merchandise.", loss, currency(loss));
        box->owt = weight(box); /* mbag_item_gone() doesn't update this */
        if (targetbox)
            targetbox->owt = weight(targetbox);
        if (srcheld || dstheld)
            encumber_msg();
    }

    if (srcheld || dstheld)
        update_inventory();
}

#if 0
staticfn int count_target_containers(struct obj *, struct obj *);

/* returns number of containers in object chain; does not recurse into
   containers; skips bags of tricks when they're known */
staticfn int
count_target_containers(
    struct obj *olist,   /* list of objects (invent) */
    struct obj *excludo) /* particular object to exclude if found in list */
{
    int ret = 0;

    while (olist) {
        if (olist != excludo && Is_container(olist)
            /* include bag of tricks when not known to be such */
            && (box->otyp != BAG_OF_TRICKS || !box->dknown
                || !objects[box->otyp].oc_name_known))
            ret++;
        olist = olist->nobj;
    }
    return ret;
}
#endif

/* ask user for a carried container into which they want box to be emptied;
   cancelled is TRUE if user cancelled the menu pick; hands aren't required
   when tipping to the floor but are when tipping into another container */
staticfn struct obj *
tipcontainer_gettarget(
    struct obj *box,
    boolean *cancelled)
{
    int n, n_conts;
    winid win;
    anything any;
    char buf[BUFSZ];
    menu_item *pick_list = (menu_item *) 0;
    struct obj dummyobj, *otmp;
    boolean hands_available = TRUE, exclude_it;
    int clr = NO_COLOR;

#if 0   /* [skip potential early return so that menu response is needed
         *  regardless of whether other containers are being carried] */
    int n_conts = count_target_containers(gi.invent, box);

    if (n_conts < 1 || !u_handsy()) {
        if (n_conts >= 1)
            pline("Tipping contents to floor only...");
        *cancelled = FALSE;
        return (struct obj *) 0;
    }
#endif

    win = create_nhwindow(NHW_MENU);
    start_menu(win, MENU_BEHAVE_STANDARD);

    dummyobj = cg.zeroobj; /* lint suppression; only its address matters */
    any = cg.zeroany;
    any.a_obj = &dummyobj;
    /* tip to floor does not require free hands */
    add_menu(win, &nul_glyphinfo, &any, '-', 0, ATR_NONE, clr,
             /* [TODO? vary destination string depending on surface()] */
             "on the floor", MENU_ITEMFLAGS_SELECTED);
    add_menu_str(win, "");

    n_conts = 0;
    for (otmp = gi.invent; otmp; otmp = otmp->nobj) {
        if (otmp == box)
            continue;
        /* skip non-containers; bag of tricks passes Is_container() test,
           only include it if it isn't known to be a bag of tricks */
        if (!Is_container(otmp)
            || (otmp->otyp == BAG_OF_TRICKS && otmp->dknown
                && objects[otmp->otyp].oc_name_known))
            continue;
        if (!n_conts++)
            hands_available = u_handsy(); /* might issue message */
        /* container-to-container tip requires free hands;
           exclude container as possible target when known to be locked */
        exclude_it = !hands_available || (otmp->olocked && otmp->lknown);
        any = cg.zeroany;
        any.a_obj = !exclude_it ? otmp : 0;
        Sprintf(buf, "%s%s", !exclude_it ? "" : "    ", doname(otmp));
        add_menu(win, &nul_glyphinfo, &any, !exclude_it ? otmp->invlet : 0, 0,
                 ATR_NONE, clr, buf, MENU_ITEMFLAGS_NONE);
    }

    Sprintf(buf, "Where to tip the contents of %s", doname(box));
    end_menu(win, buf);
    n = select_menu(win, PICK_ONE, &pick_list);
    destroy_nhwindow(win);

    otmp = 0;
    if (pick_list) {
        otmp = pick_list[0].item.a_obj;
        /* PICK_ONE with a preselected item might return 2;
           if so, choose the one that wasn't preselected */
        if (n > 1 && otmp == &dummyobj)
            otmp = pick_list[1].item.a_obj;
        if (otmp == &dummyobj)
            otmp = 0;
        free((genericptr_t) pick_list);
    }
    *cancelled = (boolean) (n == -1);
    return otmp;
}

/* Perform check on box if we can tip it.
   Returns one of TIPCHECK_foo values.
   If allowempty if TRUE, return TIPCHECK_OK instead of TIPCHECK_EMPTY. */
staticfn int
tipcontainer_checks(
    struct obj *box,       /* container player wants to tip */
    struct obj *targetbox, /* destination (used here for horn of plenty) */
    boolean allowempty)    /* affects result when box is empty */
{
    /* undiscovered bag of tricks is acceptable as a container-to-container
       destination but it can't receive items; it has to be opened in
       preparation so apply it once before even trying to tip source box */
    if (targetbox && targetbox->otyp == BAG_OF_TRICKS) {
        int seencount = 0;

        bagotricks(targetbox, FALSE, &seencount);
        return TIPCHECK_CANNOT;
    }

    /* caveat: this assumes that cknown, lknown, olocked, and otrapped
       fields haven't been overloaded to mean something special for the
       non-standard "container" horn of plenty */
    if (!box->lknown) {
        box->lknown = 1;
        if (carried(box))
            update_inventory(); /* jumping the gun slightly; hope that's ok */
    }

    if (box->olocked) {
        pline("%s is locked.", upstart(thesimpleoname(box)));
        return TIPCHECK_LOCKED;

    } else if (box->otrapped) {
        /* we're not reaching inside but we're still handling it... */
        (void) chest_trap(box, HAND, FALSE);
        /* even if the trap fails, you've used up this turn */
        if (gm.multi >= 0) { /* in case we didn't become paralyzed */
            nomul(-1);
            gm.multi_reason = "tipping a container";
            gn.nomovemsg = "";
        }
        return TIPCHECK_TRAPPED;

    } else if (box->otyp == BAG_OF_TRICKS || box->otyp == HORN_OF_PLENTY) {
        int res = TIPCHECK_OK;
        boolean bag = (box->otyp == BAG_OF_TRICKS);
        int old_spe = box->spe, seen, totseen;
        boolean maybeshopgoods = (!carried(box)
                                  && costly_spot(box->ox, box->oy));
        coordxy ox = u.ux, oy = u.uy;

        if (targetbox
            && ((res = tipcontainer_checks(targetbox, NULL, TRUE))
                != TIPCHECK_OK))
            return res;

        if (get_obj_location(box, &ox, &oy, 0))
            box->ox = ox, box->oy = oy;

        if (maybeshopgoods && !box->no_charge)
            addtobill(box, FALSE, FALSE, TRUE);
        /* apply this bag/horn until empty or monster/object creation fails
           (if the latter occurs, force the former...) */
        seen = totseen = 0;
        do {
            if (!(bag ? bagotricks(box, TRUE, &seen)
                      : hornoplenty(box, TRUE, targetbox)))
                break;
            totseen += seen;
        } while (box->spe > 0);

        if (box->spe < old_spe) {
            if (bag && !totseen)
                pline1(nothing_seems_to_happen);
            /* check_unpaid wants to see a non-zero charge count */
            box->spe = old_spe;
            check_unpaid_usage(box, TRUE);
            box->spe = 0; /* empty */
            box->cknown = 1;
        }
        if (maybeshopgoods && !box->no_charge)
            subfrombill(box, shop_keeper(*in_rooms(ox, oy, SHOPBASE)));
        return TIPCHECK_CANNOT; /* actually means 'already done' */

    } else if (SchroedingersBox(box)) {
        char yourbuf[BUFSZ];
        boolean empty_it = FALSE;

        observe_quantum_cat(box, TRUE, TRUE);
        if (!Has_contents(box)) /* evidently a live cat came out */
            /* container type of "large box" is inferred */
            pline("%sbox is now empty.", Shk_Your(yourbuf, box));
        else /* holds cat corpse */
            empty_it = TRUE;
        box->cknown = 1;
        return (empty_it || allowempty) ? TIPCHECK_OK : TIPCHECK_EMPTY;

    } else if (!allowempty && !Has_contents(box)) {
        box->cknown = 1;
        pline("%s is empty.", upstart(thesimpleoname(box)));
        return TIPCHECK_EMPTY;

    }

    return TIPCHECK_OK;
}

/*pickup.c*/
