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
This commit is contained in:
PatR
2022-05-04 19:13:28 -07:00
parent d1b14e08c4
commit 44d5be6eb4
9 changed files with 282 additions and 31 deletions
+6 -2
View File
@@ -9,8 +9,6 @@ autoopen walking into a door attempts to open it [True]
autopickup automatically pick up objects you move over [True] autopickup automatically pick up objects you move over [True]
autoquiver when firing with an empty quiver, select some [False] autoquiver when firing with an empty quiver, select some [False]
suitable inventory weapon to fill the quiver 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] BIOS allow the use of IBM ROM BIOS calls [False]
blind your character is permanently blind [False] blind your character is permanently blind [False]
bones allow loading bones files [True] 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: 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 [`] boulder override the default boulder symbol [`]
disclose the types of information you want [ni na nv ng nc no] disclose the types of information you want [ni na nv ng nc no]
offered at the end of the game offered at the end of the game
+22 -3
View File
@@ -3659,9 +3659,28 @@ If no weapon is found or the option is
false, the \(oqt\(cq (throw) command is executed instead. false, the \(oqt\(cq (throw) command is executed instead.
Persistent. Persistent.
.lp autounlock .lp autounlock
Walking into a locked door or looting a locked container while carrying Controls what action to take when attempting to walk into a locked door
an unlocking tool (such as a key) will ask whether to use that tool to or to loot a locked container.
unlock the door or container (default true). 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. Persistent.
.lp blind .lp blind
Start the character permanently blind (default false). Start the character permanently blind (default false).
+25 -3
View File
@@ -3970,9 +3970,31 @@ If no weapon is found or the option is
false, the `t' (throw) command is executed instead. Persistent. false, the `t' (throw) command is executed instead. Persistent.
%.lp %.lp
\item[\ib{autounlock}] \item[\ib{autounlock}]
Walking into a locked door or looting a locked container while carrying Controls what action to take when attempting to walk into a locked door
an unlocking tool (such as a key) will ask whether to use that tool to or to loot a locked container.
unlock the door or container (default true). 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. Persistent.
%.lp %.lp
\item[\ib{blind}] \item[\ib{blind}]
+5 -1
View File
@@ -20,7 +20,6 @@ struct flag {
boolean autodig; /* MRKR: Automatically dig */ boolean autodig; /* MRKR: Automatically dig */
boolean autoquiver; /* Automatically fill quiver */ boolean autoquiver; /* Automatically fill quiver */
boolean autoopen; /* open doors by walking into them */ boolean autoopen; /* open doors by walking into them */
boolean autounlock; /* automatically apply unlocking tools */
boolean beginner; /* True early in each game; affects feedback */ boolean beginner; /* True early in each game; affects feedback */
boolean biff; /* enable checking for mail */ boolean biff; /* enable checking for mail */
boolean bones; /* allow saving/loading bones */ boolean bones; /* allow saving/loading bones */
@@ -63,6 +62,11 @@ struct flag {
boolean tombstone; /* print tombstone */ boolean tombstone; /* print tombstone */
boolean verbose; /* max battle info */ boolean verbose; /* max battle info */
int end_top, end_around; /* describe desired score list */ 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 moonphase;
unsigned long suppress_alert; unsigned long suppress_alert;
#define NEW_MOON 0 #define NEW_MOON 0
+5 -2
View File
@@ -124,8 +124,11 @@ opt_##a,
set_in_game, No, Yes, No, NoAlias, "edit autopickup exceptions") set_in_game, No, Yes, No, NoAlias, "edit autopickup exceptions")
NHOPTB(autoquiver, 0, opt_in, set_in_game, Off, Yes, No, No, NoAlias, NHOPTB(autoquiver, 0, opt_in, set_in_game, Off, Yes, No, No, NoAlias,
&flags.autoquiver) &flags.autoquiver)
NHOPTB(autounlock, 0, opt_out, set_in_game, On, Yes, No, No, NoAlias, NHOPTC(autounlock,
&flags.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) #if defined(MICRO) && !defined(AMIGA)
NHOPTB(BIOS, 0, opt_in, set_in_config, Off, Yes, No, No, NoAlias, NHOPTB(BIOS, 0, opt_in, set_in_config, Off, Yes, No, No, NoAlias,
&iflags.BIOS) &iflags.BIOS)
+1 -1
View File
@@ -17,7 +17,7 @@
* Incrementing EDITLEVEL can be used to force invalidation of old bones * Incrementing EDITLEVEL can be used to force invalidation of old bones
* and save files. * and save files.
*/ */
#define EDITLEVEL 57 #define EDITLEVEL 58
/* /*
* Development status possibilities. * Development status possibilities.
+29 -15
View File
@@ -350,18 +350,19 @@ DISABLE_WARNING_FORMAT_NONLITERAL
/* player is applying a key, lock pick, or credit card */ /* player is applying a key, lock pick, or credit card */
int int
pick_lock(struct obj *pick, pick_lock(
xchar rx, xchar ry, /* coordinates of doors/container, struct obj *pick,
for autounlock: does not prompt xchar rx, xchar ry, /* coordinates of door/container, for autounlock:
for direction if these are set */ * does not prompt for direction if these are set */
struct obj *container) /* container, for autounlock */ struct obj *container) /* container, for autounlock */
{ {
int picktyp, c, ch; int picktyp, c, ch;
coord cc; coord cc;
struct rm *door; struct rm *door;
struct obj *otmp; struct obj *otmp;
char qbuf[QBUFSZ]; 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; picktyp = pick->otyp;
@@ -422,7 +423,7 @@ pick_lock(struct obj *pick,
boolean it; boolean it;
int count; 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.", There("isn't any sort of lock up %s.",
Levitation ? "here" : "there"); Levitation ? "here" : "there");
return PICKLOCK_LEARNED_SOMETHING; return PICKLOCK_LEARNED_SOMETHING;
@@ -436,10 +437,12 @@ pick_lock(struct obj *pick,
count = 0; count = 0;
c = 'n'; /* in case there are no boxes here */ c = 'n'; /* in case there are no boxes here */
for (otmp = g.level.objects[cc.x][cc.y]; otmp; otmp = otmp->nexthere) 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 /* autounlock on boxes: only the one that was just discovered to
* locked. Don't include any other boxes which might be here. */ be locked; don't include any other boxes which might be here */
if ((!autounlock && Is_box(otmp)) || (otmp == container)) { if (autounlock && otmp != container)
continue;
if (Is_box(otmp)) {
++count; ++count;
if (!can_reach_floor(TRUE)) { if (!can_reach_floor(TRUE)) {
You_cant("reach %s from up here.", the(xname(otmp))); You_cant("reach %s from up here.", the(xname(otmp)));
@@ -508,6 +511,7 @@ pick_lock(struct obj *pick,
g.xlock.door = 0; g.xlock.door = 0;
break; break;
} }
}
if (c != 'y') { if (c != 'y') {
if (!count) if (!count)
There("doesn't seem to be any sort of lock here."); There("doesn't seem to be any sort of lock here.");
@@ -626,6 +630,11 @@ doforce(void)
register int c, picktyp; register int c, picktyp;
char qbuf[QBUFSZ]; char qbuf[QBUFSZ];
/*
* TODO?
* allow force with edged weapon to be performed on doors.
*/
if (u.uswallow) { if (u.uswallow) {
You_cant("force anything from inside here."); You_cant("force anything from inside here.");
return ECMD_OK; return ECMD_OK;
@@ -780,7 +789,6 @@ doopen_indir(int x, int y)
if (!(door->doormask & D_CLOSED)) { if (!(door->doormask & D_CLOSED)) {
const char *mesg; const char *mesg;
boolean locked = FALSE; boolean locked = FALSE;
struct obj* unlocktool;
switch (door->doormask) { switch (door->doormask) {
case D_BROKEN: case D_BROKEN:
@@ -798,11 +806,17 @@ doopen_indir(int x, int y)
break; break;
} }
pline("This door%s.", mesg); pline("This door%s.", mesg);
if (locked) { if (locked && flags.autounlock) {
if (flags.autounlock && (unlocktool = autokey(TRUE)) != 0) { 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, res = pick_lock(unlocktool, cc.x, cc.y,
(struct obj *) 0) ? ECMD_TIME : ECMD_OK; (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_ec(dokick);
cmdq_add_dir(sgn(cc.x - u.ux), sgn(cc.y - u.uy), 0); cmdq_add_dir(sgn(cc.x - u.ux), sgn(cc.y - u.uy), 0);
res = ECMD_TIME; res = ECMD_TIME;
+179
View File
@@ -190,6 +190,14 @@ static NEARDATA const char *msgwind[][3] = { /* 'msg_window' settings */
" most recent first]" } " most recent first]" }
}; };
#endif #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[] = { static NEARDATA const char *burdentype[] = {
"unencumbered", "burdened", "stressed", "unencumbered", "burdened", "stressed",
"strained", "overtaxed", "overloaded" "strained", "overtaxed", "overloaded"
@@ -297,6 +305,7 @@ static int count_apes(void);
static int count_cond(void); static int count_cond(void);
static int handler_align_misc(int); static int handler_align_misc(int);
static int handler_autounlock(int);
static int handler_disclose(void); static int handler_disclose(void);
static int handler_menu_headings(void); static int handler_menu_headings(void);
static int handler_menustyle(void); static int handler_menustyle(void);
@@ -712,6 +721,107 @@ optfn_altkeyhandling(
return optn_ok; 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 static int
optfn_boulder(int optidx UNUSED, int req, boolean negated UNUSED, optfn_boulder(int optidx UNUSED, int req, boolean negated UNUSED,
char *opts, char *op UNUSED) char *opts, char *op UNUSED)
@@ -4408,6 +4518,75 @@ handler_align_misc(int optidx)
return optn_ok; 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 static int
handler_disclose(void) handler_disclose(void)
{ {
+10 -4
View File
@@ -1872,8 +1872,6 @@ do_loot_cont(struct obj **cobjp,
if (!cobj) if (!cobj)
return ECMD_OK; return ECMD_OK;
if (cobj->olocked) { if (cobj->olocked) {
struct obj *unlocktool;
if (ccount < 2 && (g.level.objects[cobj->ox][cobj->oy] == cobj)) if (ccount < 2 && (g.level.objects[cobj->ox][cobj->oy] == cobj))
pline("%s locked.", pline("%s locked.",
cobj->lknown ? "It is" : "Hmmm, it turns out to be"); cobj->lknown ? "It is" : "Hmmm, it turns out to be");
@@ -1884,10 +1882,18 @@ do_loot_cont(struct obj **cobjp,
cobj->lknown = 1; cobj->lknown = 1;
if (flags.autounlock) { 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 */ /* pass ox and oy to avoid direction prompt */
return (pick_lock(unlocktool, cobj->ox, cobj->oy, cobj) != 0); 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... */ /* single container, and we could #force it open... */
cmdq_add_ec(doforce); /* doforce asks for confirmation */ cmdq_add_ec(doforce); /* doforce asks for confirmation */
g.abort_looting = TRUE; g.abort_looting = TRUE;