From 4fae45220e5f2d372c236f11172fabcf13a904ea Mon Sep 17 00:00:00 2001 From: Alex Smith Date: Wed, 18 Mar 2026 21:08:22 +0000 Subject: [PATCH] Remember price quotes that have been seen for types of item These are displayed in discoveries, and a new 'price_quotes' option allows them to be displayed for un-IDed objects in other contexts too (the idea is that you turn on the option while identifying objects and off for general play). Invalidates existing save files. --- dat/opthelp | 1 + doc/Guidebook.mn | 9 ++++++ doc/fixes3-7-0.txt | 3 ++ doc/options.txt | 13 ++++++--- include/extern.h | 2 ++ include/flag.h | 1 + include/objclass.h | 5 ++++ include/objects.h | 2 +- include/optlist.h | 3 ++ include/patchlevel.h | 2 +- src/o_init.c | 20 +++++++++---- src/objnam.c | 7 +++++ src/options.c | 1 + src/shk.c | 67 +++++++++++++++++++++++++++++++++++++++++++- 14 files changed, 124 insertions(+), 12 deletions(-) diff --git a/dat/opthelp b/dat/opthelp index 1fcb803d9..a5c7d9111 100644 --- a/dat/opthelp +++ b/dat/opthelp @@ -51,6 +51,7 @@ null allow nulls to be sent to your terminal [True] perm_invent keep inventory in a permanent window [False] pickup_stolen override pickup_types for stolen objects [True] pickup_thrown override pickup_types for thrown objects [True] +price_quotes show remembered price quotes for unIDed items [False] pushweapon when wielding a new weapon, put your previously [False] wielded weapon into the secondary weapon slot quick_farsight usually skip the chance to browse the map when [False] diff --git a/doc/Guidebook.mn b/doc/Guidebook.mn index 20caca85a..9fd2c8d05 100644 --- a/doc/Guidebook.mn +++ b/doc/Guidebook.mn @@ -4592,6 +4592,15 @@ user name (on multi-user systems) or specifying a particular character name (on single-user systems) or it might be disabled entirely. Requesting it when not allowed or not possible results in explore mode instead. Default is normal play. +.lp price_quotes +Whenever the game mentions the name of an object you haven't identified yet, +it also mentions the range of buy and sell prices you have seen for that +item (to help narrow down what it could be). +The price shown is the unit price for one item (even when you are looking at +a stack of multiple items). +Many players may want to turn this on while identifying objects, and then +turn it back off again for general play. +Default is off. .lp pushweapon Using the \(oqw\(cq (wield) command when already wielding something pushes the old item into your alternate weapon slot (default off). diff --git a/doc/fixes3-7-0.txt b/doc/fixes3-7-0.txt index 9a8bd5dc4..431a7e113 100644 --- a/doc/fixes3-7-0.txt +++ b/doc/fixes3-7-0.txt @@ -2901,6 +2901,9 @@ archeologists can identify scrolls by deciphering their labels monster destroy armor -spell first erodes armor iron shoes protect the wearer against certain floor-based traps ring of stealth prevents hero from leaving tracks +the game now automatically tracks which sell prices and buy prices you have + seen for each type of item; these are visible in the discoveries list, + and can also be shown elsewhere using the new 'price_quotes' option Platform- and/or Interface-Specific New Features diff --git a/doc/options.txt b/doc/options.txt index 8cc665b7e..71a4dd6bc 100644 --- a/doc/options.txt +++ b/doc/options.txt @@ -40,9 +40,14 @@ To add an entirely new option macro type: iii) an initialization of an element in the allopt[] array, at index opt_xxxx from step ii (xxxx is the option name). - 2. Create the optfn_xxxx() function in options.c. Failure to do that will - result in a link error of "undefined function optfn_xxxx." The functions are - in options.c in alphabetical sequence by function name. + 2. If you are adding a boolean option, link it to a global variable + (e.g. in flags or iflags) and update optfn_boolean in options.c if + necessary. + + 3. If you are not adding a boolean option, create the optfn_xxxx() + function in options.c. Failure to do that will result in a link error + of "undefined function optfn_xxxx." The functions are in options.c in + alphabetical sequence by function name. The skeletal template for an optn_xxxx() function is: @@ -83,7 +88,7 @@ To add an entirely new option macro type: return optn_ok; } - 3. NOTE: If you add (or delete) an option, please update the short + 4. NOTE: If you add (or delete) an option, please update the short options help (option_help()), the long options help (dat/opthelp) and also the Guidebooks. diff --git a/include/extern.h b/include/extern.h index 0af1dfda7..7c5dd9a49 100644 --- a/include/extern.h +++ b/include/extern.h @@ -2860,6 +2860,8 @@ extern void bclose(int); /* setpaid() has a conditional code block near the end of the function, where arg1 is tested for NULL, preventing NONNULLARG1 */ extern void setpaid(struct monst *) NO_NNARGS; +extern void record_price_quote(int, unsigned long, boolean); +extern void append_price_quote(char *, char **, int) NONNULLARG12; extern long money2mon(struct monst *, long) NONNULLARG1; extern void money2u(struct monst *, long) NONNULLARG1; extern void shkgone(struct monst *) NONNULLARG1; diff --git a/include/flag.h b/include/flag.h index dbcccd312..c251236f0 100644 --- a/include/flag.h +++ b/include/flag.h @@ -330,6 +330,7 @@ struct instance_flags { boolean num_pad; /* use numbers for movement commands */ boolean perm_invent; /* display persistent inventory window */ boolean perm_invent_pending; /* need to try again */ + boolean pricequotes; /* display price quotes on unIDd objects */ boolean renameallowed; /* can change hero name during role selection */ boolean renameinprogress; /* we are changing hero name */ boolean sounds; /* master on/off switch for using soundlib */ diff --git a/include/objclass.h b/include/objclass.h index b04fa496a..f7896487e 100644 --- a/include/objclass.h +++ b/include/objclass.h @@ -104,6 +104,11 @@ struct objclass { #define oc_level oc_oc2 /* books: spell level */ unsigned short oc_nutrition; /* food value */ + + unsigned long oc_sell_minseen; + unsigned long oc_sell_maxseen; + unsigned long oc_buy_minseen; + unsigned long oc_buy_maxseen; }; struct class_sym { diff --git a/include/objects.h b/include/objects.h index 1108f14b0..60ca4396c 100644 --- a/include/objects.h +++ b/include/objects.h @@ -44,7 +44,7 @@ #define OBJECT(obj,bits,prp,sym,prob,dly,wt, \ cost,sdam,ldam,oc1,oc2,nut,color,sn) \ { 0, 0, (char *) 0, bits, prp, sym, dly, color, prob, wt, \ - cost, sdam, ldam, oc1, oc2, nut } + cost, sdam, ldam, oc1, oc2, nut, -1UL, 0, -1UL, 0 } #define MARKER(tag,sn) /*empty*/ #elif defined(OBJECTS_ENUM) diff --git a/include/optlist.h b/include/optlist.h index 0625b1e51..b920048ac 100644 --- a/include/optlist.h +++ b/include/optlist.h @@ -589,6 +589,9 @@ static int optfn_##a(int, int, boolean, char *, char *); NHOPTB(preload_tiles, Advanced, 0, opt_out, set_in_config, /* MSDOS only */ On, Yes, No, No, NoAlias, &iflags.wc_preload_tiles, Term_False, (char *)0) + NHOPTB(price_quotes, General, 0, opt_in, set_in_game, + Off, Yes, No, No, NoAlias, &iflags.pricequotes, Term_False, + "display prices you have seen for unidentified objects") NHOPTB(pushweapon, Behavior, 0, opt_in, set_in_game, Off, Yes, No, No, NoAlias, &flags.pushweapon, Term_False, "previous weapon goes to secondary slot") diff --git a/include/patchlevel.h b/include/patchlevel.h index 9dde5579e..64ff46ee3 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 132 +#define EDITLEVEL 133 /* * Development status possibilities. diff --git a/src/o_init.c b/src/o_init.c index 26732a624..67fd4d21d 100644 --- a/src/o_init.c +++ b/src/o_init.c @@ -688,16 +688,20 @@ disco_typename(int otyp) return result; } -/* append typename(dis) to buf[], possibly truncating in the process */ +/* append typename(dis) to buf[], possibly truncating in the process; + also append price quote information if it fits */ staticfn void disco_append_typename(char *buf, int dis) { - unsigned len = (unsigned) strlen(buf); + size_t len = strlen(buf); char *p, *typnm = disco_typename(dis); + size_t typnm_len = strlen(typnm); + char *eos; - if (len + (unsigned) strlen(typnm) < BUFSZ) { + if (len + typnm_len < BUFSZ) { /* ordinary */ Strcat(buf, typnm); + eos = buf + len + typnm_len; } else if ((p = strrchr(typnm, '(')) != 0 && p > typnm && p[-1] == ' ' && strchr(p, ')') != 0) { /* typename() returned "really long user-applied name (actual type)" @@ -706,10 +710,14 @@ disco_append_typename(char *buf, int dis) --p; /* back up to space in front of open paren */ (void) strncat(buf, typnm, BUFSZ - 1 - (len + (unsigned) strlen(p))); Strcat(buf, p); + eos = buf + strlen(buf); } else { /* unexpected; just truncate from end of typename */ (void) strncat(buf, typnm, BUFSZ - 1 - len); + eos = buf + strlen(buf); } + + append_price_quote(buf, &eos, dis); } /* minor fixup for Book of the Dead needed in more than one place */ @@ -1125,6 +1133,7 @@ rename_disco(void) anything any; menu_item *selected = 0; int clr = NO_COLOR; + char buf[BUFSZ]; any = cg.zeroany; tmpwin = create_nhwindow(NHW_MENU); @@ -1158,9 +1167,10 @@ rename_disco(void) prev_class = oclass; } any.a_int = dis; + *buf = '\0'; + disco_append_typename(buf, dis); add_menu(tmpwin, &nul_glyphinfo, &any, 0, 0, - ATR_NONE, clr, - disco_typename(dis), MENU_ITEMFLAGS_NONE); + ATR_NONE, clr, buf, MENU_ITEMFLAGS_NONE); } } if (ct == 0) { diff --git a/src/objnam.c b/src/objnam.c index 762b9c25d..7c5a2d637 100644 --- a/src/objnam.c +++ b/src/objnam.c @@ -1659,6 +1659,8 @@ doname_base( Sprintf(pricebuf, "%ld %s", quotedprice, currency(quotedprice)); ConcatF2(bp, 0, " (%s, %s)", obj->unpaid ? "unpaid" : "contents", pricebuf); + + record_price_quote(obj->otyp, quotedprice / obj->quan, TRUE); } else if (with_price) { /* on floor or in container on floor */ int nochrg = 0; long price = get_cost_of_shop_item(obj, &nochrg); @@ -1672,6 +1674,11 @@ doname_base( } else if (nochrg > 0) { Concat(bp, 0, " (no charge)"); } + + if (price) + record_price_quote(obj->otyp, price / obj->quan, TRUE); + } else if (iflags.pricequotes && !objects[obj->otyp].oc_name_known) { + append_price_quote(bp, &bp_eos, obj->otyp); } if (!strncmp(prefix, "a ", 2)) { diff --git a/src/options.c b/src/options.c index 090300fe1..d63db5ba7 100644 --- a/src/options.c +++ b/src/options.c @@ -5334,6 +5334,7 @@ optfn_boolean( disp.botl = TRUE; break; case opt_fixinv: + case opt_price_quotes: case opt_sortpack: case opt_implicit_uncursed: case opt_wizweight: diff --git a/src/shk.c b/src/shk.c index 4b1aedc1e..927352971 100644 --- a/src/shk.c +++ b/src/shk.c @@ -433,6 +433,65 @@ setpaid(struct monst *shkp) } } +/* Remembers that a shopkeeper has quoted a particular price for a + particular type of object. */ +void +record_price_quote(int otyp, unsigned long price, boolean buyprice) { + struct objclass *oc = &objects[otyp]; + if (buyprice) { + if (price > oc->oc_buy_maxseen) oc->oc_buy_maxseen = price; + if (price < oc->oc_buy_minseen) oc->oc_buy_minseen = price; + } else { + if (price > oc->oc_sell_maxseen) oc->oc_sell_maxseen = price; + if (price < oc->oc_sell_minseen) oc->oc_sell_minseen = price; + } +} + +/* Appends price-quote information to the given buffer, updating the + given end of string position. *eos mut be buf + strlen(buf). If the + update would make bug longer than BUFSZ, instead does nothing. */ +void +append_price_quote(char *buf, char **eos, int otyp) { + char buf2[BUFSZ]; + char *eos2 = buf2; + const char *sep = ""; + size_t len = *eos - buf; + size_t len2; + + if (objects[otyp].oc_sell_minseen > objects[otyp].oc_sell_maxseen && + objects[otyp].oc_buy_minseen > objects[otyp].oc_buy_maxseen) + return; + + eos2 += sprintf(eos2, " {"); + + if (objects[otyp].oc_buy_minseen < objects[otyp].oc_buy_maxseen) { + eos2 += sprintf(eos2, "buy %lu-%lu", + objects[otyp].oc_buy_minseen, + objects[otyp].oc_buy_maxseen); + sep = " "; + } else if (objects[otyp].oc_buy_minseen == objects[otyp].oc_buy_maxseen) { + eos2 += sprintf(eos2, "buy %lu", + objects[otyp].oc_buy_minseen); + sep = " "; + } + + if (objects[otyp].oc_sell_minseen < objects[otyp].oc_sell_maxseen) { + eos2 += sprintf(eos2, "%ssell %lu-%lu", sep, + objects[otyp].oc_sell_minseen, + objects[otyp].oc_sell_maxseen); + } else if (objects[otyp].oc_sell_minseen == objects[otyp].oc_sell_maxseen) { + eos2 += sprintf(eos2, "%ssell %lu", sep, + objects[otyp].oc_sell_minseen); + } + + eos2 += sprintf(eos2, "}"); + len2 = eos2 - buf2; + if (len2 < BUFSZ - len - 1) { + Strcpy(*eos, buf2); + *eos += len2; + } +} + staticfn long addupbill(struct monst *shkp) { @@ -3285,7 +3344,7 @@ add_one_tobill( bp->bo_id = obj->o_id; bp->bquan = obj->quan; if (dummy) { /* a dummy object must be inserted into */ - bp->useup = TRUE; /* the gb.billobjs chain here. crucial for */ + bp->useup = TRUE; /* the gb.billobjs chain here. crucial for */ add_to_billobjs(obj); /* eating floorfood in shop. see eat.c */ } else bp->useup = FALSE; @@ -3300,6 +3359,7 @@ add_one_tobill( } eshkp->billct++; obj->unpaid = 1; + record_price_quote(obj->otyp, bp->price, TRUE); } staticfn void @@ -3492,6 +3552,9 @@ addtobill( if (!Deaf && !muteshk(shkp) && !silent) { char buf[BUFSZ]; + /* no need to update price quotes here; it was done by + add_one_tobill above */ + if (!ltmp) { pline("%s has no interest in %s.", Shknam(shkp), the(xname(obj))); return; @@ -3991,6 +4054,7 @@ sellobj( pline("%s cannot pay you at present.", Shknam(shkp)); Sprintf(qbuf, "Will you accept %ld %s in credit for ", tmpcr, currency(tmpcr)); + record_price_quote(obj->otyp, tmpcr / obj->quan, FALSE); c = ynaq(safe_qbuf(qbuf, qbuf, "?", obj, doname, thesimpleoname, (obj->quan == 1L) ? "that" : "those")); if (c == 'a') { @@ -4086,6 +4150,7 @@ sellobj( : and_its_contents) : "", one ? "it" : "them"); + record_price_quote(obj->otyp, offer / obj->quan, FALSE); (void) safe_qbuf(qbuf, qbuf, qsfx, obj, xname, simpleonames, one ? "that" : "those"); } else