diff --git a/doc/fixes36.2 b/doc/fixes36.2 index bb35fa432..aafdcfd25 100644 --- a/doc/fixes36.2 +++ b/doc/fixes36.2 @@ -80,6 +80,9 @@ status_hilite options which use comparisons may now use <= and >= in addition to previous < and >; in 3.6.1 the latter operated as if they were <= and >= but now behave as conventional less than and greater than; old highlight rules using them should be updated +sortloot option has been enhanced to improve object ordering; primarily, + items of undiscovered type come out before items of discovered type + within each class or sub-class of objects Code Cleanup and Reorganization diff --git a/include/hack.h b/include/hack.h index e94e43250..0d962e5e3 100644 --- a/include/hack.h +++ b/include/hack.h @@ -193,8 +193,12 @@ enum hmon_atkmode_types { /* sortloot() return type; needed before extern.h */ struct sortloot_item { struct obj *obj; + char *str; /* result of loot_xname(obj) in some cases, otherwise null */ int indx; /* signed int, because sortloot()'s qsort comparison routine assumes (a->indx - b->indx) might yield a negative result */ + xchar class; /* order rather than object class; 0 => not yet init'd */ + xchar subclass; /* subclass for some classes */ + xchar disco; /* discovery status */ }; typedef struct sortloot_item Loot; diff --git a/src/invent.c b/src/invent.c index 5522d7b09..87a000228 100644 --- a/src/invent.c +++ b/src/invent.c @@ -9,6 +9,8 @@ #define CONTAINED_SYM '>' /* designator for inside a container */ #define HANDS_SYM '-' +STATIC_DCL void FDECL(loot_classify, (Loot *, struct obj *)); +STATIC_DCL char *FDECL(loot_xname, (struct obj *)); STATIC_DCL int FDECL(CFDECLSPEC sortloot_cmp, (const genericptr, const genericptr)); STATIC_DCL void NDECL(reorder_invent); @@ -46,7 +48,210 @@ static int lastinvnr = 51; /* 0 ... 51 (never saved&restored) */ */ static char venom_inv[] = { VENOM_CLASS, 0 }; /* (constant) */ -unsigned sortlootmode = 0; +/* sortloot() classification; called at most once for each object sorted */ +STATIC_OVL void +loot_classify(sort_item, obj) +Loot *sort_item; +struct obj *obj; +{ + /* we may eventually make this a settable option to always use + with sortloot instead of only when the 'sortpack' option isn't + set; it is similar to sortpack's inv_order but items most + likely to be picked up are moved to the front */ + static char def_srt_order[MAXOCLASSES] = { + COIN_CLASS, AMULET_CLASS, RING_CLASS, WAND_CLASS, POTION_CLASS, + SCROLL_CLASS, SPBOOK_CLASS, GEM_CLASS, FOOD_CLASS, TOOL_CLASS, + WEAPON_CLASS, ARMOR_CLASS, ROCK_CLASS, BALL_CLASS, CHAIN_CLASS, 0, + }; + static char armcat[8]; + const char *classorder; + char *p; + int k, otyp = obj->otyp, oclass = obj->oclass; + + /* + * For the value types assigned by this classification, sortloot() + * will put lower valued ones before higher valued ones. + */ + if (!Blind) + obj->dknown = 1; /* xname(obj) does this; we want it sooner */ + /* class order */ + classorder = flags.sortpack ? flags.inv_order : def_srt_order; + p = index(classorder, oclass); + if (p) + k = 1 + (int) (p - classorder); + else + k = 1 + (int) strlen(classorder) + (oclass != VENOM_CLASS); + sort_item->class = (xchar) k; + /* subclass designation; only a few classes have subclasses + and the non-armor ones we use are fairly arbitrary */ + switch (oclass) { + case ARMOR_CLASS: + if (!armcat[7]) { + /* one-time init; we use a different order than the subclass + values defined by objclass.h */ + armcat[ARM_HELM] = 1; /* [2] */ + armcat[ARM_GLOVES] = 2; /* [3] */ + armcat[ARM_BOOTS] = 3; /* [4] */ + armcat[ARM_SHIELD] = 4; /* [1] */ + armcat[ARM_CLOAK] = 5; /* [5] */ + armcat[ARM_SHIRT] = 6; /* [6] */ + armcat[ARM_SUIT] = 7; /* [0] */ + armcat[7] = 8; /* sanity protection */ + } + k = objects[otyp].oc_armcat; + /* oc_armcat overloads oc_subtyp which is an 'schar' so guard + against somebody assigning something unexpected to it */ + if (k < 0 || k >= 7) + k = 7; + k = armcat[k]; + break; + case WEAPON_CLASS: + /* for weapons, group by ammo (arrows, bolts), launcher (bows), + missile (darts, boomerangs), stackable (daggers, knives, spears), + 'other' (swords, axes, &c), polearms */ + k = objects[otyp].oc_skill; + k = (k < 0) ? ((k >= -P_CROSSBOW && k <= -P_BOW) ? 1 : 3) + : ((k >= P_BOW && k <= P_CROSSBOW) ? 2 + : (k == P_SPEAR || k == P_DAGGER || k == P_KNIFE) ? 4 + : !is_pole(obj) ? 5 : 6); + break; + case TOOL_CLASS: + if (obj->dknown && objects[otyp].oc_name_known + && (otyp == BAG_OF_TRICKS || otyp == HORN_OF_PLENTY)) + k = 2; /* known pseudo-container */ + else if (Is_container(obj)) + k = 1; /* regular container or unknown bag of tricks */ + else + switch (otyp) { + case WOODEN_FLUTE: + case MAGIC_FLUTE: + case TOOLED_HORN: + case FROST_HORN: + case FIRE_HORN: + case WOODEN_HARP: + case MAGIC_HARP: + case BUGLE: + case LEATHER_DRUM: + case DRUM_OF_EARTHQUAKE: + case HORN_OF_PLENTY: /* not a musical instrument */ + k = 3; /* instrument or unknown horn of plenty */ + default: + k = 4; /* 'other' tool */ + } + break; + case FOOD_CLASS: + /* [what about separating "partly eaten" within each group?] */ + switch (otyp) { + case SLIME_MOLD: + k = 1; + break; + default: + /* [maybe separate one-bite foods from rations and such?] */ + k = obj->globby ? 6 : 2; + break; + case TIN: + k = 3; + break; + case EGG: + k = 4; + break; + case CORPSE: + k = 5; + break; + } + break; + default: + /* other classes don't have subclasses; we assign a nonzero + value because sortloot() uses 0 to mean 'not yet classified' */ + k = 1; /* any non-zero would do */ + break; + } + sort_item->subclass = (xchar) k; + /* discovery status */ + k = !obj->dknown ? 1 /* unseen */ + : (objects[otyp].oc_name_known || !OBJ_DESCR(objects[otyp])) ? 4 + : (objects[otyp].oc_uname)? 3 /* named (partially discovered) */ + : 2; /* undiscovered */ + sort_item->disco = (xchar) k; +} + +/* sortloot() formatting routine; for alphabetizing, not shown to user */ +STATIC_OVL char * +loot_xname(obj) +struct obj *obj; +{ + struct obj saveo; + boolean save_debug; + char *res, *save_oname; + + /* + * Deal with things that xname() includes as a prefix. We don't + * want such because they change alphabetical ordering. First, + * remember 'obj's current settings. + */ + saveo.odiluted = obj->odiluted; + saveo.blessed = obj->blessed, saveo.cursed = obj->cursed; + saveo.spe = obj->spe; + saveo.owt = obj->owt; + save_oname = has_oname(obj) ? ONAME(obj) : 0; + save_debug = flags.debug; + /* suppress "diluted" for potions and "holy/unholy" for water; + sortloot() will deal with them using other criteria than name */ + if (obj->oclass == POTION_CLASS) { + obj->odiluted = 0; + if (obj->otyp == POT_WATER) + obj->blessed = 0, obj->cursed = 0; + } + /* make "wet towel" and "moist towel" format as "towel" so that all + three group together */ + if (obj->otyp == TOWEL) + obj->spe = 0; + /* group " glob of " by rather than by */ + if (obj->globby) + obj->owt = 200; /* 200: weight of combined glob from ten creatures + (five or fewer is "small", more than fifteen is + "large", in between has no prefix) */ + /* suppress user-assigned name */ + if (save_oname && !obj->oartifact) + ONAME(obj) = 0; + flags.debug = FALSE; /* avoid wizard mode formatting variations */ + + res = cxname_singular(obj); + + flags.debug = save_debug; + /* restore the object */ + if (obj->oclass == POTION_CLASS) { + obj->odiluted = saveo.odiluted; + if (obj->otyp == POT_WATER) + obj->blessed = saveo.blessed, obj->cursed = saveo.cursed; + } + if (obj->otyp == TOWEL) { + obj->spe = saveo.spe; + /* give "towel" a suffix that will force wet ones to come first, + moist ones next, and dry ones last regardless of whether + they've been flagged as having spe known */ + Strcat(res, is_wet_towel(obj) ? ((obj->spe >= 3) ? "x" : "y") : "z"); + } + if (obj->globby) { + obj->owt = saveo.owt; + /* we've suppressed the size prefix (above); there normally won't + be more than one of a given creature type because they coalesce, + but globs with different bless/curse state won't merge so it is + feasible to have multiple at the same location; add a suffix to + get such sorted by size (small first) */ + Strcat(res, (obj->owt <= 100) ? "a" + : (obj->owt <= 300) ? "b" + : (obj->owt <= 500) ? "c" + : "d"); + } + if (save_oname && !obj->oartifact) + ONAME(obj) = save_oname; + + return res; +} + +/* set by sortloot() for use by sortloot_cmp(); reset by sortloot when done */ +static unsigned sortlootmode = 0; /* qsort comparison routine for sortloot() */ STATIC_OVL int CFDECLSPEC @@ -57,57 +262,46 @@ const genericptr vptr2; struct sortloot_item *sli1 = (struct sortloot_item *) vptr1, *sli2 = (struct sortloot_item *) vptr2; struct obj *obj1 = sli1->obj, - *obj2 = sli2->obj, - sav1, sav2; - char *cls1, *cls2, nam1[BUFSZ], nam2[BUFSZ]; + *obj2 = sli2->obj; + char *nam1, *nam2; int val1, val2, c, namcmp; - /* order by object class like inventory display */ - if ((sortlootmode & SORTLOOT_PACK) != 0) { - cls1 = index(flags.inv_order, obj1->oclass); - cls2 = index(flags.inv_order, obj2->oclass); - if (cls1 != cls2) - return (int) (cls1 - cls2); + /* order by object class unless we're doing by-invlet without sortpack */ + if ((sortlootmode & (SORTLOOT_PACK | SORTLOOT_INVLET)) + != SORTLOOT_INVLET) { + /* Classify each object at most once no matter how many + comparisons it is involved in. */ + if (!sli1->class) + loot_classify(sli1, obj1); + if (!sli2->class) + loot_classify(sli2, obj2); - if ((sortlootmode & SORTLOOT_INVLET) != 0) { - ; /* skip sub-classes when sorting by packorder+invlet */ + /* Sort by class. */ + val1 = sli1->class; + val2 = sli2->class; + if (val1 != val2) + return (int) (val1 - val2); - /* for armor, group by sub-category */ - } else if (obj1->oclass == ARMOR_CLASS) { - static int armcat[7 + 1]; - - if (!armcat[7]) { - /* one-time init; we want to control the order */ - armcat[ARM_HELM] = 1; /* [2] */ - armcat[ARM_GLOVES] = 2; /* [3] */ - armcat[ARM_BOOTS] = 3; /* [4] */ - armcat[ARM_SHIELD] = 4; /* [1] */ - armcat[ARM_CLOAK] = 5; /* [5] */ - armcat[ARM_SHIRT] = 6; /* [6] */ - armcat[ARM_SUIT] = 7; /* [0] */ - armcat[7] = 8; - } - val1 = armcat[objects[obj1->otyp].oc_armcat]; - val2 = armcat[objects[obj2->otyp].oc_armcat]; + /* skip sub-classes when ordering by sortpack+invlet */ + if ((sortlootmode & SORTLOOT_INVLET) == 0) { + /* Class matches; sort by subclass. */ + val1 = sli1->subclass; + val2 = sli2->subclass; if (val1 != val2) return val1 - val2; - /* for weapons, group by ammo (arrows, bolts), launcher (bows), - missile (dart, boomerang), stackable (daggers, knives, spears), - 'other' (swords, axes, &c), polearm */ - } else if (obj1->oclass == WEAPON_CLASS) { - val1 = objects[obj1->otyp].oc_skill; - val1 = (val1 < 0) - ? (val1 >= -P_CROSSBOW && val1 <= -P_BOW) ? 1 : 3 - : (val1 >= P_BOW && val1 <= P_CROSSBOW) ? 2 - : (val1 == P_SPEAR || val1 == P_DAGGER - || val1 == P_KNIFE) ? 4 : !is_pole(obj1) ? 5 : 6; - val2 = objects[obj2->otyp].oc_skill; - val2 = (val2 < 0) - ? (val2 >= -P_CROSSBOW && val2 <= -P_BOW) ? 1 : 3 - : (val2 >= P_BOW && val2 <= P_CROSSBOW) ? 2 - : (val2 == P_SPEAR || val2 == P_DAGGER - || val2 == P_KNIFE) ? 4 : !is_pole(obj2) ? 5 : 6; + /* Class and subclass match; sort by discovery status: + * first unseen, then seen but not named or discovered, + * then named, lastly discovered. + * 1) potion + * 2) pink potion + * 3) dark green potion called confusion + * 4) potion of healing + * Multiple entries within each group will be put into + * alphabetical order below. + */ + val1 = sli1->disco; + val2 = sli2->disco; if (val1 != val2) return val1 - val2; } @@ -136,28 +330,16 @@ const genericptr vptr2; /* * Sort object names in lexicographical order, ignoring quantity. + * + * Each obj gets formatted at most once (per sort) no matter how many + * comparisons it gets subjected to. */ - /* Force diluted potions to come out after undiluted of same type; - obj->odiluted overloads obj->oeroded. */ - sav1.odiluted = obj1->odiluted; - sav2.odiluted = obj2->odiluted; - if (obj1->oclass == POTION_CLASS) - obj1->odiluted = 0; - if (obj1->oclass == POTION_CLASS) - obj2->odiluted = 0; - /* Force holy and unholy water to sort adjacent to water rather - than among 'h's and 'u's. BUCX order will keep them distinct. */ - Strcpy(nam1, cxname_singular(obj1)); - if (obj1->otyp == POT_WATER && obj1->bknown - && (obj1->blessed || obj1->cursed)) - (void) strsubst(nam1, obj1->blessed ? "holy " : "unholy ", ""); - Strcpy(nam2, cxname_singular(obj2)); - if (obj2->otyp == POT_WATER && obj2->bknown - && (obj2->blessed || obj2->cursed)) - (void) strsubst(nam2, obj2->blessed ? "holy " : "unholy ", ""); - obj1->odiluted = sav1.odiluted; - obj2->odiluted = sav2.odiluted; - + nam1 = sli1->str; + if (!nam1) + nam1 = sli1->str = dupstr(loot_xname(obj1)); + nam2 = sli2->str; + if (!nam2) + nam2 = sli2->str = dupstr(loot_xname(obj2)); if ((namcmp = strcmpi(nam1, nam2)) != 0) return namcmp; @@ -275,10 +457,14 @@ boolean FDECL((*filterfunc), (OBJ_P)); || !touch_petrifies(&mons[o->corpsenm]))) continue; sliarray[i].obj = o, sliarray[i].indx = (int) i; + sliarray[i].str = (char *) 0; + sliarray[i].class = sliarray[i].subclass = sliarray[i].disco = 0; } n = i; /* add a terminator so that we don't have to pass 'n' back to caller */ sliarray[n].obj = (struct obj *) 0, sliarray[n].indx = -1; + sliarray[n].str = (char *) 0; + sliarray[n].class = sliarray[n].subclass = sliarray[n].disco = 0; mode &= ~SORTLOOT_PETRIFY; /* do the sort; if no sorting is requested, we'll just return @@ -287,6 +473,10 @@ boolean FDECL((*filterfunc), (OBJ_P)); sortlootmode = mode; /* extra input for sortloot_cmp() */ qsort((genericptr_t) sliarray, n, sizeof *sliarray, sortloot_cmp); sortlootmode = 0; /* reset static mode flags */ + /* if sortloot_cmp formatted any objects, discard their strings now */ + for (i = 0; i < n; ++i) + if (sliarray[i].str) + free((genericptr_t) sliarray[i].str), sliarray[i].str = 0; } return sliarray; }