]> granicus.if.org Git - nethack/commitdiff
sortloot - enhanced sorting [re-revamp anyone?]
authorPatR <rankin@nethack.org>
Tue, 12 Jun 2018 23:33:35 +0000 (16:33 -0700)
committerPatR <rankin@nethack.org>
Tue, 12 Jun 2018 23:33:35 +0000 (16:33 -0700)
When objects are in the same class, sortloot orders them by their
formatted name.  It was reformatting each object every time it got
compared to another object.  Change that to remember the formatted
name so that any given object is formatted at most once (during the
current sort; future sorts will need to format it again).

Armor and weapon classes are subdivided into smaller subclasses
and the formatting plus alpha compare is only done for items in
the same subclass, so helms come out before cloaks and don't get
their names compared, for instance.  [That was from my 'revamp'
rather than the original implementation.]  This adds a couple more
subclass sets:  food (named fruit, 'other' food, tins, eggs, corpses,
globs) and tools (containers, pseudo-containers [bag of tricks and
horn of plenty once those have become discovered; prior to discovery,
bag of tricks is classified as a container and horn of plenty as an
instrument], instruments, 'other' tools).

The main difference, aside from the formatting efficiency improvement,
is to change the previous sort order
| pink potion
| potion of enlightenment
| purple-red potion
to be
| pink potion
| purple-red potion
| potion of enlightenment
by grouping undiscovered items before discovered items when class and
subclass match.  So discovery state is essentially a sub-subclass and
formatting plus string comparison is only done for members of the
same sub-subclass.  There are actually four state values:  unseen
(which applies to particular objects rather than to their type),
unknown (not discovered and not named), named (not discovered but has
player-assigned type name), and discovered (either fully discovered
or considered not interesting to discover [no alternate description,
not nameable]).

My testing was primarily done with pickup ('m,' with menustyle:T)
and sortloot:Loot (the default) plus !sortpack (not the default and
not a setting I ordinarily use, but less verbose without the class
separators).  It won't astonish me if oddities crop up with other
usage combinations.

doc/fixes36.2
include/hack.h
src/invent.c

index bb35fa432479d3d45e89c8077d2bd88bde3aa321..aafdcfd2563c7778c952d673b68a64f86248a982 100644 (file)
@@ -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
index e94e432507bf3f84bc7b2ae1105be163c1e5d0f6..0d962e5e3e14d863cdcb128c23421d28a8ceaff6 100644 (file)
@@ -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;
 
index 5522d7b09dac96a5c132141f362e33b0a35745c6..87a000228b764d16ffead8ce8b08f8c781326297 100644 (file)
@@ -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 "<size> glob of <foo>" by <foo> rather than by <size> */
+    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);
-
-        if ((sortlootmode & SORTLOOT_INVLET) != 0) {
-            ; /* skip sub-classes when sorting by packorder+invlet */
-
-        /* 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];
+    /* 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);
+
+        /* Sort by class. */
+        val1 = sli1->class;
+        val2 = sli2->class;
+        if (val1 != val2)
+            return (int) (val1 - val2);
+
+        /* 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;
 }