From 63a78edcfa50062980dd8cd4c3fba1c7ba2efe84 Mon Sep 17 00:00:00 2001 From: Pasi Kallinen Date: Sat, 21 Mar 2026 11:52:36 +0200 Subject: [PATCH] Add toggle extended command Allows the user to configure a key binding to toggle any boolean option, for example: BIND=':toggle(price_quotes) BIND=v:toggle(autodig) The option must be settable in-game. --- doc/Guidebook.mn | 12 +++++ doc/Guidebook.tex | 15 +++++++ doc/fixes3-7-0.txt | 1 + include/extern.h | 2 + include/func_tab.h | 2 + src/cmd.c | 106 +++++++++++++++++++++++++++++++++++++++++---- src/options.c | 41 ++++++++++++++++-- 7 files changed, 167 insertions(+), 12 deletions(-) diff --git a/doc/Guidebook.mn b/doc/Guidebook.mn index 9fd2c8d05..b948331b0 100644 --- a/doc/Guidebook.mn +++ b/doc/Guidebook.mn @@ -1754,6 +1754,18 @@ floor container menu. .lp "" Autocompletes. Default key is \(oqM-T\(cq. +.lp "#toggle " +Toggle a boolean option on or off. +Requires a parameter in parenthesis, the name of the option to toggle. +The option must be settable in-game. +.lp "" +For example: +.sd +.ft CR +BIND=':toggle(price_quotes) +BIND=@:toggle(autopickup) +.ft +.ed .lp "#travel " Travel to a specific location on the map. Default key is \(oq_\(cq. \" underscore diff --git a/doc/Guidebook.tex b/doc/Guidebook.tex index 62fc927a4..1c7841ae6 100644 --- a/doc/Guidebook.tex +++ b/doc/Guidebook.tex @@ -1826,6 +1826,21 @@ floor container menu. \\ %.lp "" Autocompletes. Default key is `\texttt{M-T}'. +%.lp +\item[\#toggle] +Toggle a boolean option on or off. +Requires a parameter in parenthesis, the name of the option to toggle. +The option must be settable in-game. + +%.lp "" +For example: +%.sd +\begin{verbatim} + BIND=':toggle(price_quotes) + BIND=@:toggle(autopickup) +\end{verbatim} +%.ed + %.lp \item[\#travel] Travel to a specific location on the map. diff --git a/doc/fixes3-7-0.txt b/doc/fixes3-7-0.txt index d5b973c6f..c314d079c 100644 --- a/doc/fixes3-7-0.txt +++ b/doc/fixes3-7-0.txt @@ -2912,6 +2912,7 @@ the game now automatically tracks which sell prices and buy prices you have new shields: shield of shock resistance, shield of drain resistance new wand: wand of stasis early-game monsters always miss with their first use of an attack wand +new command #toggle which can be used to toggle a boolean option Platform- and/or Interface-Specific New Features diff --git a/include/extern.h b/include/extern.h index 998e7698c..39e70fb0b 100644 --- a/include/extern.h +++ b/include/extern.h @@ -371,6 +371,7 @@ extern void change_palette(void); /* ### cmd.c ### */ extern void cmdbind_freeall(void); +extern int dotoggleoption(void); extern void set_move_cmd(int, int); extern int do_move_west(void); extern int do_move_northwest(void); @@ -2285,6 +2286,7 @@ extern char *get_option_value(const char *, boolean) NONNULLARG1; extern int doset_simple(void); extern int doset(void); extern int dotogglepickup(void); +extern int toggle_bool_option(const char *); extern void option_help(void); extern void all_options_strbuf(strbuf_t *) NONNULLARG1; extern void next_opt(winid, const char *) NONNULLARG2; diff --git a/include/func_tab.h b/include/func_tab.h index b5d1f9704..e5438ce58 100644 --- a/include/func_tab.h +++ b/include/func_tab.h @@ -22,6 +22,7 @@ #define MOUSECMD 0x0800 /* cmd allowed to be bound to mouse button */ #define CMD_INSANE 0x1000 /* suppress sanity check (for ^P and ^R) */ #define AUTOCOMP_ADJ 0x2000 /* user changed command autocompletion */ +#define CMD_PARAM 0x4000 /* command requires a param from key bind */ /* flags for extcmds_match() */ #define ECM_NOFLAGS 0 @@ -33,6 +34,7 @@ struct Cmd_bind { uchar key; boolean userbind; /* added by user */ + char *param; const struct ext_func_tab *cmd; struct Cmd_bind *next; }; diff --git a/src/cmd.c b/src/cmd.c index b9cd4c799..46200881f 100644 --- a/src/cmd.c +++ b/src/cmd.c @@ -1368,6 +1368,21 @@ dolookaround(void) return ECMD_OK; } +/* #toggle extended command + + BIND=':toggle(price_quotes) + BIND=@:toggle(autopickup) */ +int +dotoggleoption(void) +{ + if (gc.cmd_bind && gc.cmd_bind->param) { + return toggle_bool_option(gc.cmd_bind->param); + } else { + pline("Use #optionsfull to set any option instead."); + return ECMD_OK; + } +} + void set_move_cmd(int dir, int run) { @@ -1889,6 +1904,8 @@ struct ext_func_tab extcmdlist[] = { wiz_timeout_queue, IFBURIED | AUTOCOMPLETE | WIZMODECMD, NULL }, { M('T'), "tip", "empty a container", dotip, AUTOCOMPLETE | CMD_M_PREFIX, NULL }, + { '\0', "toggle", "toggle boolean option", + dotoggleoption, IFBURIED | GENERALCMD | CMD_PARAM, NULL }, { '_', "travel", "travel to a specific location on the map", dotravel, CMD_M_PREFIX, NULL }, { M('t'), "turn", "turn undead away", @@ -2119,11 +2136,16 @@ cmdbind_add(uchar key, const struct ext_func_tab *extcmd, boolean user) if (bind) { bind->cmd = extcmd; bind->userbind = user; + if (bind->param) { + free(bind->param); + bind->param = NULL; + } return; } else { bind = (struct Cmd_bind *) alloc(sizeof(struct Cmd_bind)); bind->key = key; bind->userbind = user; + bind->param = NULL; bind->cmd = extcmd; bind->next = gc.Cmd.cmdbinds; gc.Cmd.cmdbinds = bind; @@ -2142,6 +2164,8 @@ cmdbind_remove(uchar key) prev->next = bind->next; else gc.Cmd.cmdbinds = bind->next; + if (bind->param) + free(bind->param); free(bind); return; } @@ -2157,6 +2181,8 @@ cmdbind_freeall(void) while (gc.Cmd.cmdbinds) { next = gc.Cmd.cmdbinds->next; + if (gc.Cmd.cmdbinds->param) + free(gc.Cmd.cmdbinds->param); free(gc.Cmd.cmdbinds); gc.Cmd.cmdbinds = next; } @@ -2222,9 +2248,15 @@ get_changed_key_binds(strbuf_t *sbuf) while (bind) { keys[bind->key] = 1; if (bind->userbind && bind->cmd && bind->cmd->key != bind->key) { - Sprintf(buf, "BIND=%s:%s%s", key2txt(bind->key, buf2), - bind->cmd->ef_txt, - sbuf ? "\n" : ""); + if ((bind->cmd->flags & CMD_PARAM) != 0) + Sprintf(buf, "BIND=%s:%s(%s)%s", key2txt(bind->key, buf2), + bind->cmd->ef_txt, + bind->param, + sbuf ? "\n" : ""); + else + Sprintf(buf, "BIND=%s:%s%s", key2txt(bind->key, buf2), + bind->cmd->ef_txt, + sbuf ? "\n" : ""); if (sbuf) strbuf_append(sbuf, buf); else @@ -2319,18 +2351,31 @@ handler_rebind_keys_add(boolean keyfirst) destroy_nhwindow(win); if (npick > 0) { struct Cmd_bind *prevcmd; - const char *cmdstr; + char cmdstr[BUFSZ]; i = picks->item.a_int; free((genericptr_t) picks); if (i == -1) { ec = NULL; - cmdstr = "nothing"; + Strcat(cmdstr, "nothing"); goto bindit; } else { ec = &extcmdlist[i-1]; - cmdstr = ec->ef_txt; + + if ((ec->flags & CMD_PARAM) != 0) { + char parambuf[BUFSZ]; + char querybuf[BUFSZ]; + + parambuf[0] = '\0'; + Sprintf(querybuf, "Command %s requires a parameter:", ec->ef_txt); + getlin(querybuf, parambuf); + (void) mungspaces(parambuf); + Snprintf(cmdstr, BUFSZ-1, "%s(%s)", ec->ef_txt, parambuf); + cmdstr[BUFSZ-1] = '\0'; + } else { + Strcat(cmdstr, ec->ef_txt); + } } bindit: if (!key) { @@ -2615,6 +2660,8 @@ boolean bind_key(uchar key, const char *command, boolean user) { struct ext_func_tab *extcmd; + long len; + char *buf, *p = NULL, *lastp = NULL; /* special case: "nothing" is reserved for unbinding */ if (!strcmpi(command, "nothing")) { @@ -2622,12 +2669,46 @@ bind_key(uchar key, const char *command, boolean user) return TRUE; } + /* copy command to buf for modification */ + len = strlen(command) + 1; + buf = (char *)alloc(len); + (void) strncpy(buf, command, len); + + /* does buf have a parameter in parenthesis? */ + if ((p = strchr(buf, '(')) != 0 + && (lastp = strrchr(buf, ')')) != 0 + && (lastp > p)) { + *p = '\0'; + *lastp = '\0'; + /* p points to the parameter */ + p++; + } + for (extcmd = extcmdlist; extcmd->ef_txt; extcmd++) { - if (strcmpi(command, extcmd->ef_txt)) + if (strcmpi(buf, extcmd->ef_txt)) continue; if ((extcmd->flags & INTERNALCMD) != 0) continue; cmdbind_add(key, extcmd, user); + + if ((extcmd->flags & CMD_PARAM) != 0) { + if (!p) { + config_error_add("'%s' requires a parameter", buf); + } else { + struct Cmd_bind *bind = cmdbind_get(key); + int maxlen = min(30, strlen(p)) + 1; + + if (maxlen <= 1) { + config_error_add("Required parameter cannot be empty"); + } else { + bind->param = (char *) alloc(maxlen); + (void) strncpy(bind->param, p, maxlen); + bind->param[maxlen-1] = '\0'; + } + } + } else if (p && strlen(p) > 0) + config_error_add("'%s' does not take a parameter", buf); + #if 0 /* silently accept key binding for unavailable command (!SHELL,&c) */ if ((extcmd->flags & CMD_NOT_AVAILABLE) != 0) { char buf[BUFSZ]; @@ -2636,9 +2717,11 @@ bind_key(uchar key, const char *command, boolean user) config_error_add("%s", buf); } #endif + free(buf); return TRUE; } + free(buf); return FALSE; } @@ -2742,8 +2825,13 @@ keylist_putcmds(winid datawin, boolean docount, count++; continue; } - Sprintf(buf, "%-7s %-13s %s", key2txt(key, buf2), - bind->cmd->ef_txt, bind->cmd->ef_desc); + if ((bind->cmd->flags & CMD_PARAM) != 0) + Sprintf(buf, "%-7s %-13s %s \"%s\"", key2txt(key, buf2), + bind->cmd->ef_txt, bind->cmd->ef_desc, + bind->param); + else + Sprintf(buf, "%-7s %-13s %s", key2txt(key, buf2), + bind->cmd->ef_txt, bind->cmd->ef_desc); putstr(datawin, 0, buf); keys_used[i] = TRUE; } diff --git a/src/options.c b/src/options.c index 51faccca8..6f5a565ae 100644 --- a/src/options.c +++ b/src/options.c @@ -390,6 +390,7 @@ staticfn boolean parse_role_opt(int, boolean, const char *, char *, char **); staticfn char *get_cnf_role_opt(int); staticfn unsigned int longest_option_name(int, int); staticfn int doset_simple_menu(void); +staticfn void reset_needed_visuals(void); staticfn void doset_add_menu(winid, const char *, const char *, int, int); staticfn int handle_add_list_remove(const char *, int); staticfn void all_options_conds(strbuf_t *); @@ -9012,6 +9013,15 @@ doset(void) /* changing options via menu by Per Liboriussen */ goto rerun; } + reset_needed_visuals(); + return ECMD_OK; +} + +#undef HELP_IDX + +staticfn void +reset_needed_visuals(void) +{ if (go.opt_need_glyph_reset) { reset_glyphmap(gm_optionchange); } @@ -9039,11 +9049,13 @@ doset(void) /* changing options via menu by Per Liboriussen */ if (disp.botl || disp.botlx) { bot(); } - return ECMD_OK; + go.opt_need_redraw = FALSE; + go.opt_need_glyph_reset = FALSE; + go.opt_reset_customcolors = FALSE; + go.opt_reset_customsymbols = FALSE; + go.opt_update_basic_palette = FALSE; } -#undef HELP_IDX - /* doset(#optionsfull command) menu entries for compound options */ staticfn void doset_add_menu( @@ -9304,6 +9316,29 @@ dotogglepickup(void) return ECMD_OK; } +/* toggle any (settable in-game) boolean option by name */ +int +toggle_bool_option(const char *p) +{ + int i; + int ret = ECMD_FAIL; + + for (i = 0; i < OPTCOUNT; i++) + if (!strncmpi(allopt[i].name, p, strlen(p)) + && allopt[i].opttyp == BoolOpt + && allopt[i].setwhere == set_in_game + && allopt[i].addr != 0) { + char buf[BUFSZ]; + + Sprintf(buf, "%s%s", *allopt[i].addr ? "!" : "", allopt[i].name); + if (parseoptions(buf, FALSE, FALSE)) + ret = ECMD_OK; + + reset_needed_visuals(); + } + return ret; +} + int add_autopickup_exception(const char *mapping) {