From 44d5be6eb4d26606f99b7eb78f11e7439f6b30d9 Mon Sep 17 00:00:00 2001 From: PatR Date: Wed, 4 May 2022 19:13:28 -0700 Subject: [PATCH] autounlock overhaul This gives the player more control over what autounlock does. It is now a compound option rather than a boolean, and takes values of autounlock:none !autounlock or noautounlock (shortcuts for none) autounlock:untrap + apply-key + kick + force (spaces are optional or can be used instead of plus-signs, but can't mix "foo bar+quux") autounlock (without a value, shortcut for autounlock:apply-key). Default is autounlock:apply-key. Untrap isn't implemented (feel free to jump in) so is suppressed from the 'O' command's new sub-menu for autounlock. It's parsed and accepted from .nethackrc but won't accomplish anything. [Just musing: it should be feasible to kick in direction '.' to break open a container or #force to an adjacent spot to break open a door. If that was done, autounlock:kick+force (or more likely autounlock: apply-key+kick+force when lacking a key) would resort to force if hero couldn't kick due to wounded legs or riding. This changes struct flags so increments EDITLEVEL again. This includes pull requests #750 from entrez and #751 from FIQ but was entered from scratch rather than using use their commits. Closes #750 Closes #751 --- dat/opthelp | 8 +- doc/Guidebook.mn | 25 +++++- doc/Guidebook.tex | 28 ++++++- include/flag.h | 6 +- include/optlist.h | 7 +- include/patchlevel.h | 2 +- src/lock.c | 44 +++++++---- src/options.c | 179 +++++++++++++++++++++++++++++++++++++++++++ src/pickup.c | 14 +++- 9 files changed, 282 insertions(+), 31 deletions(-) diff --git a/dat/opthelp b/dat/opthelp index c13294d3c..a9b604d2f 100644 --- a/dat/opthelp +++ b/dat/opthelp @@ -9,8 +9,6 @@ autoopen walking into a door attempts to open it [True] autopickup automatically pick up objects you move over [True] autoquiver when firing with an empty quiver, select some [False] suitable inventory weapon to fill the quiver -autounlock when opening a locked door or looting a locked [True] - container while carrying a key, offer to use it BIOS allow the use of IBM ROM BIOS calls [False] blind your character is permanently blind [False] bones allow loading bones files [True] @@ -150,6 +148,12 @@ Compound options are written as option_name:option_value. Compound options which can be set during the game are: +autounlock when attempting to open a door or loot a [Apply-Key] + container that is locked, specifies an action to take: + can be None, or one or more of Apply-Key + Kick + Force; + Kick is only useful for doors and Force is only useful for + containers; either will only be attempted if Apply-Key is + omitted or you aren't carrying any unlocking tool boulder override the default boulder symbol [`] disclose the types of information you want [ni na nv ng nc no] offered at the end of the game diff --git a/doc/Guidebook.mn b/doc/Guidebook.mn index 89b45f982..338f350a5 100644 --- a/doc/Guidebook.mn +++ b/doc/Guidebook.mn @@ -3659,9 +3659,28 @@ If no weapon is found or the option is false, the \(oqt\(cq (throw) command is executed instead. Persistent. .lp autounlock -Walking into a locked door or looting a locked container while carrying -an unlocking tool (such as a key) will ask whether to use that tool to -unlock the door or container (default true). +Controls what action to take when attempting to walk into a locked door +or to loot a locked container. +Takes a plus-sign separated list of values: +\fIapply-key\fP which will attempt to use a key or other unlocking tool +if you have one; +\fIkick\fP which will kick the door (if you lack a key or omit apply-key; +has no effect on containers); +\fIforce\fP which will try to force a container's lid with your currently +wielded weapon (if you lack a key or omit apply-key; has no effect on +doors); or +\fInone\fP which can't be combined with the other choices. +.lp "" +Omitting the value is treated as if \f(CRautounlock:apply-key\fP. +Preceding \f(CRautounlock\fP with \(oq!\(cq or \(lqno\(rq is treated as +\f(CRautounlock:none\fP. +.lp "" +Applying a key might set off a trap if the door or container is trapped. +Successfully kicking a door will break it and wake up nearby monsters. +Successfully forcing a container open will break its lock and might also +destroy some of its contents or damage your weapon or both. +.lp "" +The default is \fIapply-key\fP. Persistent. .lp blind Start the character permanently blind (default false). diff --git a/doc/Guidebook.tex b/doc/Guidebook.tex index f1a0d24e6..fc35a5a04 100644 --- a/doc/Guidebook.tex +++ b/doc/Guidebook.tex @@ -3970,9 +3970,31 @@ If no weapon is found or the option is false, the `t' (throw) command is executed instead. Persistent. %.lp \item[\ib{autounlock}] -Walking into a locked door or looting a locked container while carrying -an unlocking tool (such as a key) will ask whether to use that tool to -unlock the door or container (default true). +Controls what action to take when attempting to walk into a locked door +or to loot a locked container. +Takes a plus-sign separated list of values: +{\it apply-key\/} which will attempt to use a key or other unlocking tool +if you have one; +{\it kick\/} which will kick the door (if you lack a key or omit apply-key; +has no effect on containers); +{\it force\/} which will try to force a container's lid with your currently +wielded weapon (if you lack a key or omit apply-key; has no effect on +doors); or +{\it none\/} which can't be combined with the other choices. +\\ +%.lp "" +Omitting the value is treated as if {\tt autounlock:apply-key}. +Preceding {\tt autounlock} with `{\tt !}' or ``{\tt no}'' is treated as +{\tt autounlock:none}. +\\ +%.lp "" +Applying a key might set off a trap if the door or container is trapped. +Successfully kicking a door will break it and wake up nearby monsters. +Successfully forcing a container open will break its lock and might also +destroy some of its contents or damage your weapon or both. +\\ +%.lp "" +The default is {\it apply-key\/}. Persistent. %.lp \item[\ib{blind}] diff --git a/include/flag.h b/include/flag.h index 0636cd72d..400c66097 100644 --- a/include/flag.h +++ b/include/flag.h @@ -20,7 +20,6 @@ struct flag { boolean autodig; /* MRKR: Automatically dig */ boolean autoquiver; /* Automatically fill quiver */ boolean autoopen; /* open doors by walking into them */ - boolean autounlock; /* automatically apply unlocking tools */ boolean beginner; /* True early in each game; affects feedback */ boolean biff; /* enable checking for mail */ boolean bones; /* allow saving/loading bones */ @@ -63,6 +62,11 @@ struct flag { boolean tombstone; /* print tombstone */ boolean verbose; /* max battle info */ int end_top, end_around; /* describe desired score list */ + unsigned autounlock; /* locked door/chest action */ +#define AUTOUNLOCK_UNTRAP 1 +#define AUTOUNLOCK_APPLY_KEY 2 +#define AUTOUNLOCK_KICK 4 +#define AUTOUNLOCK_FORCE 8 unsigned moonphase; unsigned long suppress_alert; #define NEW_MOON 0 diff --git a/include/optlist.h b/include/optlist.h index 6adbfcb76..3dcfff79f 100644 --- a/include/optlist.h +++ b/include/optlist.h @@ -124,8 +124,11 @@ opt_##a, set_in_game, No, Yes, No, NoAlias, "edit autopickup exceptions") NHOPTB(autoquiver, 0, opt_in, set_in_game, Off, Yes, No, No, NoAlias, &flags.autoquiver) - NHOPTB(autounlock, 0, opt_out, set_in_game, On, Yes, No, No, NoAlias, - &flags.autounlock) + NHOPTC(autounlock, + (sizeof "none" + sizeof "untrap" + sizeof "apply-key" + + sizeof "kick" + sizeof "force" + 20), + opt_out, set_in_game, Yes, Yes, No, Yes, NoAlias, + "action to take when encountering locked door or chest") #if defined(MICRO) && !defined(AMIGA) NHOPTB(BIOS, 0, opt_in, set_in_config, Off, Yes, No, No, NoAlias, &iflags.BIOS) diff --git a/include/patchlevel.h b/include/patchlevel.h index b2d7b54fb..9ac519611 100644 --- a/include/patchlevel.h +++ b/include/patchlevel.h @@ -17,7 +17,7 @@ * Incrementing EDITLEVEL can be used to force invalidation of old bones * and save files. */ -#define EDITLEVEL 57 +#define EDITLEVEL 58 /* * Development status possibilities. diff --git a/src/lock.c b/src/lock.c index b2ef50799..80fdbedbc 100644 --- a/src/lock.c +++ b/src/lock.c @@ -350,18 +350,19 @@ DISABLE_WARNING_FORMAT_NONLITERAL /* player is applying a key, lock pick, or credit card */ int -pick_lock(struct obj *pick, - xchar rx, xchar ry, /* coordinates of doors/container, - for autounlock: does not prompt - for direction if these are set */ - struct obj *container) /* container, for autounlock */ +pick_lock( + struct obj *pick, + xchar rx, xchar ry, /* coordinates of door/container, for autounlock: + * does not prompt for direction if these are set */ + struct obj *container) /* container, for autounlock */ { int picktyp, c, ch; coord cc; struct rm *door; struct obj *otmp; char qbuf[QBUFSZ]; - boolean autounlock = (rx != 0 && ry != 0) || (container != NULL); + boolean autounlock = (((rx != 0 && ry != 0) || container != NULL) + && (flags.autounlock & AUTOUNLOCK_APPLY_KEY) != 0); picktyp = pick->otyp; @@ -422,7 +423,7 @@ pick_lock(struct obj *pick, boolean it; int count; - if (u.dz < 0) { + if (u.dz < 0 && !autounlock) { /* beware stale u.dz value */ There("isn't any sort of lock up %s.", Levitation ? "here" : "there"); return PICKLOCK_LEARNED_SOMETHING; @@ -436,10 +437,12 @@ pick_lock(struct obj *pick, count = 0; c = 'n'; /* in case there are no boxes here */ - for (otmp = g.level.objects[cc.x][cc.y]; otmp; otmp = otmp->nexthere) - /* autounlock on boxes: only the one that just informed you it was - * locked. Don't include any other boxes which might be here. */ - if ((!autounlock && Is_box(otmp)) || (otmp == container)) { + for (otmp = g.level.objects[cc.x][cc.y]; otmp; otmp = otmp->nexthere) { + /* autounlock on boxes: only the one that was just discovered to + be locked; don't include any other boxes which might be here */ + if (autounlock && otmp != container) + continue; + if (Is_box(otmp)) { ++count; if (!can_reach_floor(TRUE)) { You_cant("reach %s from up here.", the(xname(otmp))); @@ -508,6 +511,7 @@ pick_lock(struct obj *pick, g.xlock.door = 0; break; } + } if (c != 'y') { if (!count) There("doesn't seem to be any sort of lock here."); @@ -626,6 +630,11 @@ doforce(void) register int c, picktyp; char qbuf[QBUFSZ]; + /* + * TODO? + * allow force with edged weapon to be performed on doors. + */ + if (u.uswallow) { You_cant("force anything from inside here."); return ECMD_OK; @@ -780,7 +789,6 @@ doopen_indir(int x, int y) if (!(door->doormask & D_CLOSED)) { const char *mesg; boolean locked = FALSE; - struct obj* unlocktool; switch (door->doormask) { case D_BROKEN: @@ -798,11 +806,17 @@ doopen_indir(int x, int y) break; } pline("This door%s.", mesg); - if (locked) { - if (flags.autounlock && (unlocktool = autokey(TRUE)) != 0) { + if (locked && flags.autounlock) { + struct obj *unlocktool; + + u.dz = 0; /* should already be 0 since hero moved toward door */ + if ((flags.autounlock & AUTOUNLOCK_APPLY_KEY) != 0 + && (unlocktool = autokey(TRUE)) != 0) { res = pick_lock(unlocktool, cc.x, cc.y, (struct obj *) 0) ? ECMD_TIME : ECMD_OK; - } else if (!u.usteed && ynq("Kick it?") == 'y') { + } else if (!u.usteed + && (flags.autounlock & AUTOUNLOCK_KICK) != 0 + && ynq("Kick it?") == 'y') { cmdq_add_ec(dokick); cmdq_add_dir(sgn(cc.x - u.ux), sgn(cc.y - u.uy), 0); res = ECMD_TIME; diff --git a/src/options.c b/src/options.c index 0afce53a2..6a29cd562 100644 --- a/src/options.c +++ b/src/options.c @@ -190,6 +190,14 @@ static NEARDATA const char *msgwind[][3] = { /* 'msg_window' settings */ " most recent first]" } }; #endif +/* autounlock settings */ +static NEARDATA const char *unlocktypes[][2] = { + { "none", "" }, + { "untrap", "(might fail)" }, + { "apply-key", "" }, + { "kick", "(doors only)" }, + { "force", "(chests/boxes only)" }, +}; static NEARDATA const char *burdentype[] = { "unencumbered", "burdened", "stressed", "strained", "overtaxed", "overloaded" @@ -297,6 +305,7 @@ static int count_apes(void); static int count_cond(void); static int handler_align_misc(int); +static int handler_autounlock(int); static int handler_disclose(void); static int handler_menu_headings(void); static int handler_menustyle(void); @@ -712,6 +721,107 @@ optfn_altkeyhandling( return optn_ok; } +static int +optfn_autounlock( + int optidx, + int req, + boolean negated, + char *opts, + char *op) +{ + if (req == do_init) { + flags.autounlock = AUTOUNLOCK_APPLY_KEY; + return optn_ok; + } + if (req == do_set) { + /* autounlock:none or autounlock:untrap+apply-key+kick+force; + autounlock without a value is same as autounlock:apply-key and + !autounlock is same as autounlock:none; multiple values can be + space separated or plus-sign separated but the same separation + must be used for each element, not mix&match */ + char sep, *nxt; + unsigned newflags; + int i; + + if ((op = string_for_opt(opts, TRUE)) == empty_optstr) { + flags.autounlock = negated ? 0 : AUTOUNLOCK_APPLY_KEY; + return optn_ok; + } + newflags = 0; + sep = index(op, '+') ? '+' : ' '; + while (op) { + op = trimspaces(op); /* might have leading space */ + if ((nxt = index(op, sep)) != '\0') { + *nxt++ = '\0'; + op = trimspaces(op); /* might have trailing space after + * plus sign removal */ + } + for (i = 0; i < SIZE(unlocktypes); ++i) + if (!strncmpi(op, unlocktypes[i][0], Strlen(op)) + /* fuzzymatch() doesn't match leading substrings but + this allows "apply_key" and "applykey" to match + "apply-key"; "apply key" too if part of foo+bar */ + || fuzzymatch(op, unlocktypes[i][0], " -_", TRUE)) { + switch (*op) { + case 'n': + negated = TRUE; + break; + case 'u': + newflags |= AUTOUNLOCK_UNTRAP; + break; + case 'a': + newflags |= AUTOUNLOCK_APPLY_KEY; + break; + case 'k': + newflags |= AUTOUNLOCK_KICK; + break; + case 'f': + newflags |= AUTOUNLOCK_FORCE; + break; + default: + config_error_add("Invalid value for \"%s\": \"%s\"", + allopt[optidx].name, op); + return optn_silenterr; + } + } + op = nxt; + } + if (negated && newflags != 0) { + config_error_add( + "Invalid value combination for \"%s\": 'none' with some", + allopt[optidx].name); + return optn_silenterr; + } + flags.autounlock = newflags; + return optn_ok; + } + if (req == get_val) { + if (!opts) + return optn_err; + if (!flags.autounlock) { + Strcpy(opts, "none"); + } else { + static const char plus[] = " + "; + const char *p = ""; + + *opts = '\0'; + if (flags.autounlock & AUTOUNLOCK_UNTRAP) + Sprintf(eos(opts), "%s%s", p, unlocktypes[1][0]), p = plus; + if (flags.autounlock & AUTOUNLOCK_APPLY_KEY) + Sprintf(eos(opts), "%s%s", p, unlocktypes[2][0]), p = plus; + if (flags.autounlock & AUTOUNLOCK_KICK) + Sprintf(eos(opts), "%s%s", p, unlocktypes[3][0]), p = plus; + if (flags.autounlock & AUTOUNLOCK_FORCE) + Sprintf(eos(opts), "%s%s", p, unlocktypes[4][0]); /*no more p*/ + } + return optn_ok; + } + if (req == do_handler) { + return handler_autounlock(optidx); + } + return optn_ok; +} + static int optfn_boulder(int optidx UNUSED, int req, boolean negated UNUSED, char *opts, char *op UNUSED) @@ -4408,6 +4518,75 @@ handler_align_misc(int optidx) return optn_ok; } +static int +handler_autounlock(int optidx) +{ + winid tmpwin; + anything any; + boolean chngd; + unsigned oldflags = flags.autounlock; + const char *optname = allopt[optidx].name; + char buf[BUFSZ], sep = iflags.menu_tab_sep ? '\t' : ' '; + menu_item *window_pick = (menu_item *) 0; + int i, n, presel, res = optn_ok; + + tmpwin = create_nhwindow(NHW_MENU); + start_menu(tmpwin, MENU_BEHAVE_STANDARD); + any = cg.zeroany; + for (i = 0; i < SIZE(unlocktypes); ++i) { + if (i == 1) /*** suppress 'untrap' from the menu... ***/ + continue; /*** until it actually gets implemented ***/ + Sprintf(buf, "%-10.10s%c%.40s", + unlocktypes[i][0], sep, unlocktypes[i][1]); + presel = !i ? !flags.autounlock : (flags.autounlock & (1 << (i - 1))); + any.a_int = i + 1; + add_menu(tmpwin, &nul_glyphinfo, &any, *unlocktypes[i][0], 0, + ATR_NONE, buf, + ((presel ? MENU_ITEMFLAGS_SELECTED : MENU_ITEMFLAGS_NONE) + | (!i ? MENU_ITEMFLAGS_SKIPINVERT : 0))); + } + Sprintf(buf, "Select '%.20s' actions:", optname); + end_menu(tmpwin, buf); + n = select_menu(tmpwin, PICK_ANY, &window_pick); + if (n > 0) { + int k; + boolean wasnone = !flags.autounlock; + unsigned newflags = 0, noflags = 0; + + for (i = 0; i < n; ++i) { + k = window_pick[i].item.a_int - 1; + if (k) + newflags |= (1 << (k - 1)); + else + noflags = 1; + } + /* wasnone: 'none' is preselected; + !wasnone: don't force it to be unselected */ + if (newflags && noflags && !wasnone) { + config_error_add( + "Invalid value combination for \"%s\": 'none' with some", + optname); + res = optn_silenterr; + } else { + flags.autounlock = newflags; + } + free((genericptr_t) window_pick); + } else if (n == 0) { /* nothing was picked but menu wasn't cancelled */ + /* something that was preselected got unselected, leaving nothing; + treat that as picking 'none' (even though 'none' might be what + got unselected) */ + flags.autounlock = 0; + } + destroy_nhwindow(tmpwin); + chngd = (flags.autounlock != oldflags); + if (chngd || flags.verbose) { + optfn_autounlock(optidx, get_val, FALSE, buf, (char *) NULL); + pline("'%s' %s '%s'.", optname, + chngd ? "changed to" : "is still", buf); + } + return res; +} + static int handler_disclose(void) { diff --git a/src/pickup.c b/src/pickup.c index 7ef599045..b358040e8 100644 --- a/src/pickup.c +++ b/src/pickup.c @@ -1872,8 +1872,6 @@ do_loot_cont(struct obj **cobjp, if (!cobj) return ECMD_OK; if (cobj->olocked) { - struct obj *unlocktool; - if (ccount < 2 && (g.level.objects[cobj->ox][cobj->oy] == cobj)) pline("%s locked.", cobj->lknown ? "It is" : "Hmmm, it turns out to be"); @@ -1884,10 +1882,18 @@ do_loot_cont(struct obj **cobjp, cobj->lknown = 1; if (flags.autounlock) { - if ((unlocktool = autokey(TRUE)) != 0) { + struct obj *unlocktool; + + /* TODO: handle AUTOUNLOCK_UNTRAP and maybe add kicking at + self when chest present to handle AUTOUNLOCK_KICK */ + u.dz = 0; /* might be non-zero from previous command since + * #loot isn't a move command; pick_lock() cares */ + if ((flags.autounlock & AUTOUNLOCK_APPLY_KEY) != 0 + && (unlocktool = autokey(TRUE)) != 0) { /* pass ox and oy to avoid direction prompt */ return (pick_lock(unlocktool, cobj->ox, cobj->oy, cobj) != 0); - } else if (ccount == 1 && u_have_forceable_weapon()) { + } else if ((flags.autounlock & AUTOUNLOCK_FORCE) != 0 + && ccount == 1 && u_have_forceable_weapon()) { /* single container, and we could #force it open... */ cmdq_add_ec(doforce); /* doforce asks for confirmation */ g.abort_looting = TRUE;