/* NetHack 5.0	getpos.c	$NHDT-Date: 1763708572 2025/11/20 23:02:52 $  $NHDT-Branch: NetHack-3.7 $:$NHDT-Revision: 1.6 $ */
/*-Copyright (c) Pasi Kallinen, 2023. */
/* NetHack may be freely redistributed.  See license for details. */

#include "hack.h"

extern const char what_is_a_location[]; /* from pager.c */

staticfn void getpos_toggle_hilite_state(void);
staticfn void getpos_getvalids_selection(struct selectionvar *,
                                       boolean (*)(coordxy, coordxy));
staticfn void getpos_help_keyxhelp(winid, const char *, const char *, int);
staticfn void getpos_help(boolean, const char *);
staticfn int QSORTCALLBACK cmp_coord_distu(const void *, const void *);
staticfn int gloc_filter_classify_glyph(int);
staticfn int gloc_filter_floodfill_matcharea(coordxy, coordxy);
staticfn void gloc_filter_floodfill(coordxy, coordxy);
staticfn void gloc_filter_init(void);
staticfn void gloc_filter_done(void);
staticfn void gather_locs(coord **, int *, int);
staticfn boolean known_vibrating_square_at(coordxy, coordxy);
staticfn void truncate_to_map(coordxy *, coordxy *, schar, schar);
staticfn void getpos_refresh(void);
/* Callback function for getpos() to highlight desired map locations.
 * Parameter TRUE: initialize and highlight, FALSE: done (remove highlights).
 */
staticfn void (*getpos_hilitefunc)(boolean) = (void (*)(boolean)) 0;
staticfn boolean (*getpos_getvalid)(coordxy, coordxy)
                                          = (boolean (*)(coordxy, coordxy)) 0;
enum getposHiliteState {
    HiliteNormalMap = 0,
    HiliteGoodposSymbol = 1,
    HiliteBackground = 2,
};

static enum getposHiliteState
    getpos_hilite_state = HiliteNormalMap,
    defaultHiliteState = HiliteNormalMap;

void
getpos_sethilite(
    void (*gp_hilitef)(boolean),
    boolean (*gp_getvalidf)(coordxy, coordxy))
{
    boolean (*old_getvalid)(coordxy, coordxy) = getpos_getvalid;
    uint32 old_map_frame_color = gw.wsettings.map_frame_color;
    struct selectionvar *sel = selection_new();

    defaultHiliteState = iflags.bgcolors ? HiliteBackground : HiliteNormalMap;
    if (gp_getvalidf != old_getvalid)
        getpos_hilite_state = defaultHiliteState;

    getpos_getvalids_selection(sel, getpos_getvalid);
    getpos_hilitefunc = gp_hilitef;
    getpos_getvalid = gp_getvalidf;
    getpos_getvalids_selection(sel, getpos_getvalid);
    gw.wsettings.map_frame_color = (getpos_hilite_state == HiliteBackground)
                                   ? HI_ZAP : NO_COLOR;

    if (getpos_getvalid != old_getvalid
        || gw.wsettings.map_frame_color != old_map_frame_color)
        selection_force_newsyms(sel);
    selection_free(sel, TRUE);
}

/* cycle 'getpos_hilite_state' to its next state;
   when 'bgcolors' is Off, it will alternate between not showing valid
   positions and showing them via temporary S_goodpos symbol;
   when 'bgcolors' is On, there are three states and showing them via
   setting background color becomes the default */
staticfn void
getpos_toggle_hilite_state(void)
{
    /* getpos_hilitefunc isn't Null */
    if (getpos_hilite_state == HiliteGoodposSymbol) {
        /* currently on, finish */
        (*getpos_hilitefunc)(FALSE); /* tmp_at(DISP_END) */
    }

    getpos_hilite_state = (getpos_hilite_state + 1)
                          % (iflags.bgcolors ? 3 : 2);
    /* resetting the callback functions to their current values will draw
       valid-spots with background color if that is the new state and turn
       off that color if it was the previous state */
    getpos_sethilite(getpos_hilitefunc, getpos_getvalid);

    if (getpos_hilite_state == HiliteGoodposSymbol) {
        /* now on, begin */
        (*getpos_hilitefunc)(TRUE);
    }
}

boolean
mapxy_valid(coordxy x, coordxy y)
{
    if (getpos_getvalid)
        return (*getpos_getvalid)(x, y);
    return FALSE;
}

staticfn void
getpos_getvalids_selection(
    struct selectionvar *sel,
    boolean (*validf)(coordxy, coordxy))
{
    coordxy x, y;

    if (!sel || !validf)
        return;

    for (x = 1; x < sel->wid; x++)
        for (y = 0; y < sel->hei; y++)
            if ((*validf)(x, y))
                selection_setpoint(x, y, sel, 1);
}

static const char *const gloc_descr[NUM_GLOCS][4] = {
    { "any monsters", "monster", "next/previous monster", "monsters" },
    { "any items", "item", "next/previous object", "objects" },
    { "any doors", "door", "next/previous door or doorway",
      "doors or doorways" },
    { "any unexplored areas", "unexplored area", "unexplored location",
      "locations next to unexplored locations" },
    { "anything interesting", "interesting thing", "anything interesting",
      "anything interesting" },
    { "any valid locations", "valid location", "valid location",
      "valid locations" }
};

static const char *const gloc_filtertxt[NUM_GFILTER] = {
    "",
    " in view",
    " in this area"
};

staticfn void
getpos_help_keyxhelp(
    winid tmpwin,
    const char *k1, const char *k2,
    int gloc)
{
    char sbuf[BUFSZ], fbuf[QBUFSZ];
    const char *move_cursor_to = "move the cursor to ",
               *filtertxt = gloc_filtertxt[iflags.getloc_filter];

    if (gloc == GLOC_EXPLORE) {
        /* default of "move to unexplored location" is inaccurate
           because the position will be one spot short of that */
        move_cursor_to = "move the cursor next to an ";
        if (iflags.getloc_usemenu)
            /* default is too wide for basic 80-column tty so shorten it
               to avoid wrapping */
            filtertxt = strsubst(strcpy(fbuf, filtertxt),
                                 "this area", "area");
    }
    Sprintf(sbuf, "Use '%s'/'%s' to %s%s%s.",
            k1, k2,
            iflags.getloc_usemenu ? "get a menu of " : move_cursor_to,
            gloc_descr[gloc][2 + iflags.getloc_usemenu], filtertxt);
    putstr(tmpwin, 0, sbuf);
}

DISABLE_WARNING_FORMAT_NONLITERAL

/* the response for '?' help request in getpos() */
staticfn void
getpos_help(boolean force, const char *goal)
{
    static const char *const fastmovemode[2] = { "8 units at a time",
                                                 "skipping same glyphs" };
    char sbuf[BUFSZ];
    boolean doing_what_is;
    winid tmpwin = create_nhwindow(NHW_MENU);

    Sprintf(sbuf,
            "Use '%s', '%s', '%s', '%s' to move the cursor to %s.", /* hjkl */
            visctrl(cmd_from_func(do_move_west)),
            visctrl(cmd_from_func(do_move_south)),
            visctrl(cmd_from_func(do_move_north)),
            visctrl(cmd_from_func(do_move_east)), goal);
    putstr(tmpwin, 0, sbuf);
    Sprintf(sbuf,
            "Use '%s', '%s', '%s', '%s' to fast-move the cursor, %s.",
            visctrl(cmd_from_func(do_run_west)),
            visctrl(cmd_from_func(do_run_south)),
            visctrl(cmd_from_func(do_run_north)),
            visctrl(cmd_from_func(do_run_east)),
            fastmovemode[iflags.getloc_moveskip]);
    putstr(tmpwin, 0, sbuf);
    Sprintf(sbuf, "(or prefix normal move with '%s' or '%s' to fast-move)",
            visctrl(cmd_from_func(do_run)),
            visctrl(cmd_from_func(do_rush)));
    putstr(tmpwin, 0, sbuf);
    putstr(tmpwin, 0, "Or enter a background symbol (ex. '<').");
    Sprintf(sbuf, "Use '%s' to move the cursor on yourself.",
            visctrl(gc.Cmd.spkeys[NHKF_GETPOS_SELF]));
    putstr(tmpwin, 0, sbuf);
    if (!iflags.terrainmode || (iflags.terrainmode & TER_MON) != 0) {
        getpos_help_keyxhelp(tmpwin,
                             visctrl(gc.Cmd.spkeys[NHKF_GETPOS_MON_NEXT]),
                             visctrl(gc.Cmd.spkeys[NHKF_GETPOS_MON_PREV]),
                             GLOC_MONS);
    }
    if (goal && !strcmp(goal, "a monster"))
        goto skip_non_mons;
    if (!iflags.terrainmode || (iflags.terrainmode & TER_OBJ) != 0) {
        getpos_help_keyxhelp(tmpwin,
                             visctrl(gc.Cmd.spkeys[NHKF_GETPOS_OBJ_NEXT]),
                             visctrl(gc.Cmd.spkeys[NHKF_GETPOS_OBJ_PREV]),
                             GLOC_OBJS);
    }
    if (!iflags.terrainmode || (iflags.terrainmode & TER_MAP) != 0) {
        /* these are primarily useful when choosing a travel
           destination for the '_' command */
        getpos_help_keyxhelp(tmpwin,
                             visctrl(gc.Cmd.spkeys[NHKF_GETPOS_DOOR_NEXT]),
                             visctrl(gc.Cmd.spkeys[NHKF_GETPOS_DOOR_PREV]),
                             GLOC_DOOR);
        getpos_help_keyxhelp(tmpwin,
                             visctrl(gc.Cmd.spkeys[NHKF_GETPOS_UNEX_NEXT]),
                             visctrl(gc.Cmd.spkeys[NHKF_GETPOS_UNEX_PREV]),
                             GLOC_EXPLORE);
        getpos_help_keyxhelp(tmpwin,
                          visctrl(gc.Cmd.spkeys[NHKF_GETPOS_INTERESTING_NEXT]),
                          visctrl(gc.Cmd.spkeys[NHKF_GETPOS_INTERESTING_PREV]),
                             GLOC_INTERESTING);
    }
    Sprintf(sbuf, "Use '%s' to change fast-move mode to %s.",
            visctrl(gc.Cmd.spkeys[NHKF_GETPOS_MOVESKIP]),
            fastmovemode[!iflags.getloc_moveskip]);
    putstr(tmpwin, 0, sbuf);
    if (!iflags.terrainmode || (iflags.terrainmode & TER_DETECT) == 0) {
        Sprintf(sbuf, "Use '%s' to toggle menu listing for possible targets.",
                visctrl(gc.Cmd.spkeys[NHKF_GETPOS_MENU]));
        putstr(tmpwin, 0, sbuf);
        Sprintf(sbuf,
                "Use '%s' to change the mode of limiting possible targets.",
                visctrl(gc.Cmd.spkeys[NHKF_GETPOS_LIMITVIEW]));
        putstr(tmpwin, 0, sbuf);
    }
    if (!iflags.terrainmode) {
        char kbuf[BUFSZ];

        if (getpos_getvalid) {
            Sprintf(sbuf, "Use '%s' or '%s' to move to valid locations.",
                    visctrl(gc.Cmd.spkeys[NHKF_GETPOS_VALID_NEXT]),
                    visctrl(gc.Cmd.spkeys[NHKF_GETPOS_VALID_PREV]));
            putstr(tmpwin, 0, sbuf);
        }
        if (getpos_hilitefunc) {
            Sprintf(sbuf, "Use '%s' to toggle marking of valid locations.",
                    visctrl(gc.Cmd.spkeys[NHKF_GETPOS_SHOWVALID]));
            putstr(tmpwin, 0, sbuf);
        }
        Sprintf(sbuf, "Use '%s' to toggle automatic description.",
                visctrl(gc.Cmd.spkeys[NHKF_GETPOS_AUTODESC]));
        putstr(tmpwin, 0, sbuf);
        if (iflags.cmdassist) { /* assisting the '/' command, I suppose... */
            Sprintf(sbuf,
                    (iflags.getpos_coords == GPCOORDS_NONE)
        ? "(Set 'whatis_coord' option to include coordinates with '%s' text.)"
        : "(Reset 'whatis_coord' option to omit coordinates from '%s' text.)",
                    visctrl(gc.Cmd.spkeys[NHKF_GETPOS_AUTODESC]));
        }
 skip_non_mons:
        /* disgusting hack; the alternate selection characters work for any
           getpos call, but only matter for dowhatis (and doquickwhatis,
           also for dotherecmdmenu's simulated mouse) */
        doing_what_is = (goal == what_is_a_location);
        if (doing_what_is) {
            Sprintf(kbuf, "'%s' or '%s' or '%s' or '%s'",
                    visctrl(gc.Cmd.spkeys[NHKF_GETPOS_PICK]),
                    visctrl(gc.Cmd.spkeys[NHKF_GETPOS_PICK_Q]),
                    visctrl(gc.Cmd.spkeys[NHKF_GETPOS_PICK_O]),
                    visctrl(gc.Cmd.spkeys[NHKF_GETPOS_PICK_V]));
        } else {
            Sprintf(kbuf, "'%s'", visctrl(gc.Cmd.spkeys[NHKF_GETPOS_PICK]));
        }
        Snprintf(sbuf, sizeof(sbuf),
                 "Type a %s when you are at the right place.", kbuf);
        putstr(tmpwin, 0, sbuf);
        if (doing_what_is) {
            Sprintf(sbuf,
      "  '%s' describe current spot, show 'more info', move to another spot.",
                    visctrl(gc.Cmd.spkeys[NHKF_GETPOS_PICK_V]));
            putstr(tmpwin, 0, sbuf);
            Sprintf(sbuf,
                    "  '%s' describe current spot,%s move to another spot;",
                    visctrl(gc.Cmd.spkeys[NHKF_GETPOS_PICK]),
                    flags.help && !force ? " prompt if 'more info'," : "");
            putstr(tmpwin, 0, sbuf);
            Sprintf(sbuf,
                    "  '%s' describe current spot, move to another spot;",
                    visctrl(gc.Cmd.spkeys[NHKF_GETPOS_PICK_Q]));
            putstr(tmpwin, 0, sbuf);
            Sprintf(sbuf,
                    "  '%s' describe current spot, stop looking at things;",
                    visctrl(gc.Cmd.spkeys[NHKF_GETPOS_PICK_O]));
            putstr(tmpwin, 0, sbuf);
        }
    }
    if (!force)
        putstr(tmpwin, 0, "Type Space or Escape when you're done.");
    putstr(tmpwin, 0, "");
    display_nhwindow(tmpwin, TRUE);
    destroy_nhwindow(tmpwin);
}

RESTORE_WARNING_FORMAT_NONLITERAL

staticfn int QSORTCALLBACK
cmp_coord_distu(const void *a, const void *b)
{
    const coord *c1 = a;
    const coord *c2 = b;
    int dx, dy, dist_1, dist_2;

    dx = u.ux - c1->x;
    dy = u.uy - c1->y;
    dist_1 = max(abs(dx), abs(dy));
    dx = u.ux - c2->x;
    dy = u.uy - c2->y;
    dist_2 = max(abs(dx), abs(dy));

    if (dist_1 == dist_2)
        return (c1->y != c2->y) ? (c1->y - c2->y) : (c1->x - c2->x);

    return dist_1 - dist_2;
}

#define IS_UNEXPLORED_LOC(x,y) \
    (isok((x), (y))                                             \
     && glyph_is_unexplored(levl[(x)][(y)].glyph)               \
     && !levl[(x)][(y)].seenv)

#define GLOC_SAME_AREA(x,y) \
    (isok((x), (y))                                             \
     && (selection_getpoint((x),(y), gg.gloc_filter_map)))

staticfn int
gloc_filter_classify_glyph(int glyph)
{
    int c;

    if (!glyph_is_cmap(glyph))
        return 0;

    c = glyph_to_cmap(glyph);

    if (is_cmap_room(c) || is_cmap_furniture(c))
        return 1;
    else if (is_cmap_wall(c) || c == S_tree)
        return 2;
    else if (is_cmap_corr(c))
        return 3;
    else if (is_cmap_water(c))
        return 4;
    else if (is_cmap_lava(c))
        return 5;
    return 0;
}

staticfn int
gloc_filter_floodfill_matcharea(coordxy x, coordxy y)
{
    int glyph = back_to_glyph(x, y);

    if (!levl[x][y].seenv)
        return FALSE;

    if (glyph == gg.gloc_filter_floodfill_match_glyph)
        return TRUE;

    if (gloc_filter_classify_glyph(glyph)
        == gloc_filter_classify_glyph(gg.gloc_filter_floodfill_match_glyph))
        return TRUE;

    return FALSE;
}

staticfn void
gloc_filter_floodfill(coordxy x, coordxy y)
{
    gg.gloc_filter_floodfill_match_glyph = back_to_glyph(x, y);

    set_selection_floodfillchk(gloc_filter_floodfill_matcharea);
    selection_floodfill(gg.gloc_filter_map, x, y, FALSE);
}

staticfn void
gloc_filter_init(void)
{
    if (iflags.getloc_filter == GFILTER_AREA) {
        if (!gg.gloc_filter_map) {
            gg.gloc_filter_map = selection_new();
        }
        /* special case: if we're in a doorway, try to figure out which
           direction we're moving, and use that side of the doorway */
        if (IS_DOOR(levl[u.ux][u.uy].typ)) {
            if ((u.dx || u.dy) && isok(u.ux + u.dx, u.uy + u.dy)) {
                gloc_filter_floodfill(u.ux + u.dx, u.uy + u.dy);
            } else {
                /* TODO: maybe add both sides of the doorway? */
            }
        } else {
            gloc_filter_floodfill(u.ux, u.uy);
        }
    }
}

staticfn void
gloc_filter_done(void)
{
    if (gg.gloc_filter_map) {
        selection_free(gg.gloc_filter_map, TRUE);
        gg.gloc_filter_map = (struct selectionvar *) 0;

    }
}

staticfn boolean
known_vibrating_square_at(coordxy x, coordxy y)
{
    /* note: this only acknowledges the genuine vibrating square, not
       fake ones produced by wizard mode wishing for traps which could
       possibly be transfered to normal play via bones file */
    if (invocation_pos(x, y)) {
        struct trap *ttmp = t_at(x, y);

        return ttmp && ttmp->ttyp == VIBRATING_SQUARE && ttmp->tseen;
    }
    return FALSE;
}

DISABLE_WARNING_UNREACHABLE_CODE

boolean
gather_locs_interesting(
    coordxy x, coordxy y,
    int gloc)
{
    int glyph, sym;

    if (iflags.getloc_filter == GFILTER_VIEW && !cansee(x, y))
        return FALSE;
    if (iflags.getloc_filter == GFILTER_AREA && !GLOC_SAME_AREA(x, y)
        && !GLOC_SAME_AREA(x - 1, y) && !GLOC_SAME_AREA(x, y - 1)
        && !GLOC_SAME_AREA(x + 1, y) && !GLOC_SAME_AREA(x, y + 1))
        return FALSE;

    glyph = glyph_at(x, y);
    sym = glyph_is_cmap(glyph) ? glyph_to_cmap(glyph) : -1;
    switch (gloc) {
    default:
    case GLOC_MONS:
        /* unlike '/M', this skips monsters revealed by
           warning glyphs and remembered unseen ones */
        return (glyph_is_monster(glyph)
                && glyph != monnum_to_glyph(PM_LONG_WORM_TAIL, MALE)
                && glyph != monnum_to_glyph(PM_LONG_WORM_TAIL, FEMALE));
    case GLOC_OBJS:
        return (glyph_is_object(glyph)
                && glyph != objnum_to_glyph(BOULDER)
                && glyph != objnum_to_glyph(ROCK));
    case GLOC_DOOR:
        return (glyph_is_cmap(glyph)
                && (is_cmap_door(sym)
                    || is_cmap_drawbridge(sym)
                    || sym == S_ndoor));
    case GLOC_EXPLORE:
        return (glyph_is_cmap(glyph)
                && !glyph_is_nothing(glyph_to_cmap(glyph))
                && (is_cmap_door(sym)
                    || is_cmap_drawbridge(sym)
                    || sym == S_ndoor
                    || is_cmap_room(sym)
                    || is_cmap_corr(sym))
                && (IS_UNEXPLORED_LOC(x + 1, y)
                    || IS_UNEXPLORED_LOC(x - 1, y)
                    || IS_UNEXPLORED_LOC(x, y + 1)
                    || IS_UNEXPLORED_LOC(x, y - 1)));
    case GLOC_VALID:
        if (getpos_getvalid)
            return (*getpos_getvalid)(x, y);
        FALLTHROUGH;
        /*FALLTHRU*/
    case GLOC_INTERESTING:
        return (gather_locs_interesting(x, y, GLOC_DOOR)
                || !((glyph_is_cmap(glyph)
                      && (is_cmap_wall(sym)
                          || sym == S_tree
                          || sym == S_bars
                          || sym == S_ice
                          || sym == S_air
                          || sym == S_cloud
                          || is_cmap_lava(sym)
                          || is_cmap_water(sym)
                          || sym == S_ndoor
                          || is_cmap_room(sym)
                          || is_cmap_corr(sym)))
                     || glyph_is_nothing(glyph)
                     || glyph_is_unexplored(glyph))
                || known_vibrating_square_at(x, y));
    }
    /*NOTREACHED*/
    return FALSE;
}

RESTORE_WARNINGS

/* gather locations for monsters or objects shown on the map */
staticfn void
gather_locs(coord **arr_p, int *cnt_p, int gloc)
{
    int pass, idx;
    coordxy x, y;

    /*
     * We always include the hero's location even if there is no monster
     * (invisible hero without see invisible) or object (usual case)
     * displayed there.  That way, the count will always be at least 1,
     * and player has a visual indicator (cursor returns to hero's spot)
     * highlighting when successive 'm's or 'o's have cycled all the way
     * through all monsters or objects.
     *
     * Hero's spot will always sort to array[0] because it will always
     * be the shortest distance (namely, 0 units) away from <u.ux,u.uy>.
     */

    gloc_filter_init();

    *cnt_p = idx = 0;
    for (pass = 0; pass < 2; pass++) {
        for (x = 1; x < COLNO; x++)
            for (y = 0; y < ROWNO; y++) {
                if (u_at(x, y) || gather_locs_interesting(x, y, gloc)) {
                    if (!pass) {
                        ++*cnt_p;
                    } else {
                        (*arr_p)[idx].x = x;
                        (*arr_p)[idx].y = y;
                        ++idx;
                    }
                }
            }

        if (!pass) /* end of first pass */
            *arr_p = (coord *) alloc(*cnt_p * sizeof (coord));
        else /* end of second pass */
            qsort(*arr_p, *cnt_p, sizeof (coord), cmp_coord_distu);
    } /* pass */

    gloc_filter_done();
}

char *
dxdy_to_dist_descr(coordxy dx, coordxy dy, boolean fulldir)
{
    static char buf[30];
    int dst;

    if (!dx && !dy) {
        Sprintf(buf, "here");
    } else if ((dst = xytodir(dx, dy)) != -1) {
        /* explicit direction; 'one step' is implicit */
        Sprintf(buf, "%s", directionname(dst));
    } else {
        static const char *const dirnames[4][2] = {
            { "n", "north" },
            { "s", "south" },
            { "w", "west" },
            { "e", "east" } };
        buf[0] = '\0';
        /* 9999: protect buf[] against overflow caused by invalid values */
        if (dy) {
            if (abs(dy) > 9999)
                dy = sgn(dy) * 9999;
            Sprintf(eos(buf), "%d%s%s", abs(dy), dirnames[(dy > 0)][fulldir],
                    dx ? "," : "");
        }
        if (dx) {
            if (abs(dx) > 9999)
                dx = sgn(dx) * 9999;
            Sprintf(eos(buf), "%d%s", abs(dx),
                    dirnames[2 + (dx > 0)][fulldir]);
        }
    }
    return buf;
}

DISABLE_WARNING_FORMAT_NONLITERAL

/* coordinate formatting for 'whatis_coord' option */
char *
coord_desc(coordxy x, coordxy y, char *outbuf, char cmode)
{
    static char screen_fmt[16]; /* [12] suffices: "[%02d,%02d]" */
    int dx, dy;

    outbuf[0] = '\0';
    switch (cmode) {
    default:
        break;
    case GPCOORDS_COMFULL:
    case GPCOORDS_COMPASS:
        /* "east", "3s", "2n,4w" */
        dx = x - u.ux;
        dy = y - u.uy;
        Sprintf(outbuf, "(%s)",
                dxdy_to_dist_descr(dx, dy, cmode == GPCOORDS_COMFULL));
        break;
    case GPCOORDS_MAP: /* x,y */
        /* upper left corner of map is <1,0>;
           with default COLNO,ROWNO lower right corner is <79,20> */
        Sprintf(outbuf, "<%d,%d>", x, y);
        break;
    case GPCOORDS_SCREEN: /* y+2,x */
        /* for normal map sizes, force a fixed-width formatting so that
           /m, /M, /o, and /O output lines up cleanly; map sizes bigger
           than Nx999 or 999xM will still work, but not line up like normal
           when displayed in a column setting.

           The (100) is placed in brackets below to mark the [: "03"] as
           explicit compile-time dead code for clang */
        if (!*screen_fmt)
            Sprintf(screen_fmt, "[%%%sd,%%%sd]",
                    (ROWNO - 1 + 2 < (100)) ? "02" :  "03",
                    (COLNO - 1 < (100)) ? "02" : "03");
        /* map line 0 is screen row 2;
           map column 0 isn't used, map column 1 is screen column 1 */
        Sprintf(outbuf, screen_fmt, y + 2, x);
        break;
    }
    return outbuf;
}

RESTORE_WARNING_FORMAT_NONLITERAL

void
auto_describe(coordxy cx, coordxy cy)
{
    coord cc;
    int sym = 0;
    char tmpbuf[BUFSZ];
    const char *firstmatch = "unknown";

    cc.x = cx;
    cc.y = cy;
    if (do_screen_description(cc, TRUE, sym, tmpbuf, &firstmatch,
                              (struct permonst **) 0)) {
        (void) coord_desc(cx, cy, tmpbuf, iflags.getpos_coords);
        custompline((SUPPRESS_HISTORY | OVERRIDE_MSGTYPE | NO_CURS_ON_U),
                    "%s%s%s%s%s", firstmatch, *tmpbuf ? " " : "", tmpbuf,
                    (iflags.autodescribe
                     && getpos_getvalid && !(*getpos_getvalid)(cx, cy))
                      ? " (invalid target)" : "",
                    (iflags.getloc_travelmode && !is_valid_travelpt(cx, cy))
                      ? " (no travel path)" : "");
        curs(WIN_MAP, cx, cy);
        flush_screen(0);
    }
}

boolean
getpos_menu(coord *ccp, int gloc)
{
    coord *garr = DUMMY;
    int gcount = 0;
    winid tmpwin;
    anything any;
    int i, pick_cnt;
    menu_item *picks = (menu_item *) 0;
    char tmpbuf[BUFSZ];
    int clr = NO_COLOR;

    gather_locs(&garr, &gcount, gloc);

    if (gcount < 2) { /* gcount always includes the hero */
        free((genericptr_t) garr);
        You("cannot %s %s.",
            (iflags.getloc_filter == GFILTER_VIEW) ? "see" : "detect",
            gloc_descr[gloc][0]);
        return FALSE;
    }

    tmpwin = create_nhwindow(NHW_MENU);
    start_menu(tmpwin, MENU_BEHAVE_STANDARD);
    any = cg.zeroany;

    /* gather_locs returns array[0] == you. skip it. */
    for (i = 1; i < gcount; i++) {
        char fullbuf[BUFSZ];
        coord tmpcc;
        const char *firstmatch = "unknown";
        int sym = 0;

        any.a_int = i + 1;
        tmpcc.x = garr[i].x;
        tmpcc.y = garr[i].y;
        if (do_screen_description(tmpcc, TRUE, sym, tmpbuf,
                              &firstmatch, (struct permonst **)0)) {
            (void) coord_desc(garr[i].x, garr[i].y, tmpbuf,
                              iflags.getpos_coords);
            Snprintf(fullbuf, sizeof fullbuf, "%s%s%s", firstmatch,
                    (*tmpbuf ? " " : ""), tmpbuf);
            add_menu(tmpwin, &nul_glyphinfo, &any, 0, 0,
                     ATR_NONE, clr, fullbuf, MENU_ITEMFLAGS_NONE);
        }
    }

    Sprintf(tmpbuf, "Pick %s%s%s",
            an(gloc_descr[gloc][1]),
            gloc_filtertxt[iflags.getloc_filter],
            iflags.getloc_travelmode ? " for travel destination" : "");
    end_menu(tmpwin, tmpbuf);
    pick_cnt = select_menu(tmpwin, PICK_ONE, &picks);
    destroy_nhwindow(tmpwin);
    if (pick_cnt > 0) {
        ccp->x = garr[picks->item.a_int - 1].x;
        ccp->y = garr[picks->item.a_int - 1].y;
        free((genericptr_t) picks);
    }
    free((genericptr_t) garr);
    return (pick_cnt > 0);
}

/* add dx,dy to cx,cy, truncating at map edges */
staticfn void
truncate_to_map(coordxy *cx, coordxy *cy, schar dx, schar dy)
{
    /* diagonal moves complicate this... */
    if (*cx + dx < 1) {
        dy -= sgn(dy) * (1 - (*cx + dx));
        dx = 1 - *cx; /* so that (cx+dx == 1) */
    } else if (*cx + dx > COLNO - 1) {
        dy += sgn(dy) * ((COLNO - 1) - (*cx + dx));
        dx = (COLNO - 1) - *cx;
    }
    if (*cy + dy < 0) {
        dx -= sgn(dx) * (0 - (*cy + dy));
        dy = 0 - *cy; /* so that (cy+dy == 0) */
    } else if (*cy + dy > ROWNO - 1) {
        dx += sgn(dx) * ((ROWNO - 1) - (*cy + dy));
        dy = (ROWNO - 1) - *cy;
    }
    *cx += dx;
    *cy += dy;
}

/* called when ^R typed; if '$' is being shown for valid spots, remove that;
   if alternate background color is being shown for that, redraw it */
staticfn void
getpos_refresh(void)
{
    if (getpos_hilitefunc && getpos_hilite_state == HiliteGoodposSymbol) {
        (*getpos_hilitefunc)(FALSE); /* tmp_at(DISP_END) */
        getpos_hilite_state = defaultHiliteState;
    }

    docrt_flags(docrtRefresh);

    if (getpos_hilitefunc && getpos_hilite_state == HiliteBackground) {
        /* resetting to current values will draw valid-spots highlighting */
        getpos_sethilite(getpos_hilitefunc, getpos_getvalid);
    }
}

/* have the player use movement keystrokes to position the cursor at a
   particular map location, then use one of [.,:;] to pick the spot */
int
getpos(coord *ccp, boolean force, const char *goal)
{
    static struct {
        int nhkf, ret;
    } const pick_chars_def[] = {
        { NHKF_GETPOS_PICK, LOOK_TRADITIONAL },
        { NHKF_GETPOS_PICK_Q, LOOK_QUICK },
        { NHKF_GETPOS_PICK_O, LOOK_ONCE },
        { NHKF_GETPOS_PICK_V, LOOK_VERBOSE }
    };
    static const int mMoOdDxX_def[] = {
        NHKF_GETPOS_MON_NEXT,
        NHKF_GETPOS_MON_PREV,
        NHKF_GETPOS_OBJ_NEXT,
        NHKF_GETPOS_OBJ_PREV,
        NHKF_GETPOS_DOOR_NEXT,
        NHKF_GETPOS_DOOR_PREV,
        NHKF_GETPOS_UNEX_NEXT,
        NHKF_GETPOS_UNEX_PREV,
        NHKF_GETPOS_INTERESTING_NEXT,
        NHKF_GETPOS_INTERESTING_PREV,
        NHKF_GETPOS_VALID_NEXT,
        NHKF_GETPOS_VALID_PREV
    };
    struct _cmd_queue cq, *cmdq;
    const char *cp;
    char pick_chars[6];
    char mMoOdDxX[13];
    int result = 0;
    int i, c;
    int sidx;
    coordxy cx, cy;
    coordxy tx = u.ux, ty = u.uy;
    boolean msg_given = TRUE; /* clear message window by default */
    boolean show_goal_msg = FALSE;
    coord *garr[NUM_GLOCS] = DUMMY;
    int gcount[NUM_GLOCS] = DUMMY;
    int gidx[NUM_GLOCS] = DUMMY;
    schar udx = u.dx, udy = u.dy, udz = u.dz;
    int dx, dy;
    boolean rushrun = FALSE;

    /* temporary? if we have a queued direction, return the adjacent spot
       in that direction */
    if (!gi.in_doagain) {
        if ((cmdq = cmdq_pop()) != 0) {
            cq = *cmdq;
            free((genericptr_t) cmdq);
            if (cq.typ == CMDQ_DIR && !cq.dirz) {
                ccp->x = u.ux + cq.dirx;
                ccp->y = u.uy + cq.diry;
            } else {
                cmdq_clear(CQ_CANNED);
                result = -1;
            }
            return result;
        }
    }

    for (i = 0; i < SIZE(pick_chars_def); i++)
        pick_chars[i] = gc.Cmd.spkeys[pick_chars_def[i].nhkf];
    pick_chars[SIZE(pick_chars_def)] = '\0';

    for (i = 0; i < SIZE(mMoOdDxX_def); i++)
        mMoOdDxX[i] = gc.Cmd.spkeys[mMoOdDxX_def[i]];
    mMoOdDxX[SIZE(mMoOdDxX_def)] = '\0';

    if (handle_tip(TIP_GETPOS))
        show_goal_msg = TRUE; /* tip has overwritten prompt in mesg window */

    if (!goal)
        goal = "desired location";
    if (flags.verbose) {
        pline("(For instructions type a '%s')",
              visctrl(gc.Cmd.spkeys[NHKF_GETPOS_HELP]));
        msg_given = TRUE;
    }
    cx = gg.getposx = ccp->x;
    cy = gg.getposy = ccp->y;
#ifdef CLIPPING
    cliparound(cx, cy);
#endif
    curs(WIN_MAP, cx, cy);
    flush_screen(0);
#ifdef MACOS9
    lock_mouse_cursor(TRUE);
#endif
    lock_mouse_buttons(TRUE);
    for (;;) {
        if (show_goal_msg) {
            pline("Move cursor to %s:", goal);
            curs(WIN_MAP, cx, cy);
            flush_screen(0);
            show_goal_msg = FALSE;
        } else if (iflags.autodescribe && !msg_given) {
            auto_describe(cx, cy);
        }

        rushrun = FALSE;

        if ((cmdq = cmdq_pop()) != 0) {
            if (cmdq->typ == CMDQ_KEY) {
                c = cmdq->key;
            } else {
                cmdq_clear(CQ_CANNED);
                result = -1;
                goto exitgetpos;
            }
            free(cmdq);
        } else {
            c = readchar_poskey(&tx, &ty, &sidx);
            /* remember_getpos is normally False because reusing the
               cursor positioning during ^A is almost never the right
               thing to do, but caller could set it if that was needed */
            if (iflags.remember_getpos && !gi.in_doagain)
                cmdq_add_key(CQ_REPEAT, c);
        }

        if (iflags.autodescribe)
            msg_given = FALSE;

        if (c == gc.Cmd.spkeys[NHKF_ESC]) {
            cx = cy = -10;
            msg_given = TRUE; /* force clear */
            result = -1;
            break;
        }
        if (c == cmd_from_func(do_run) || c == cmd_from_func(do_rush)) {
            c = readchar_poskey(&tx, &ty, &sidx);
            rushrun = TRUE;
        }
        if (c == 0) {
            if (!isok(tx, ty))
                continue;
            /* a mouse click event, just assign and return */
            cx = tx;
            cy = ty;
            break;
        }
        if ((cp = strchr(pick_chars, c)) != 0) {
            /* '.' => 0, ',' => 1, ';' => 2, ':' => 3 */
            result = pick_chars_def[(int) (cp - pick_chars)].ret;
            break;
        } else if (movecmd(c, MV_WALK)) {
            if (rushrun)
                goto do_rushrun;
            dx = u.dx;
            dy = u.dy;
            truncate_to_map(&cx, &cy, dx, dy);
            goto nxtc;
        } else if (movecmd(c, MV_RUSH) || movecmd(c, MV_RUN)) {
 do_rushrun:
            if (iflags.getloc_moveskip) {
                /* skip same glyphs */
                int glyph = glyph_at(cx, cy);

                dx = u.dx;
                dy = u.dy;
                while (isok(cx + dx, cy + dy)
                       && glyph == glyph_at(cx + dx, cy + dy)
                       && isok(cx + dx + u.dx, cy + dy + u.dy)
                       && glyph == glyph_at(cx + dx + u.dx, cy + dy + u.dy)) {
                    dx += u.dx;
                    dy += u.dy;

                }
            } else {
                dx = 8 * u.dx;
                dy = 8 * u.dy;
            }
            truncate_to_map(&cx, &cy, dx, dy);
            goto nxtc;
        }

        if (c == gc.Cmd.spkeys[NHKF_GETPOS_HELP] || redraw_cmd(c)) {
            /* '?' will redraw twice, first when removing popup text window
               after showing the help text, then to reset highlighting */
            if (c == gc.Cmd.spkeys[NHKF_GETPOS_HELP])
                getpos_help(force, goal);
            /* ^R: docrt(), hilite_state = default */
            getpos_refresh();
            curs(WIN_MAP, cx, cy);
            /* update message window to reflect that we're still targeting */
            show_goal_msg = TRUE;
        } else if (c == gc.Cmd.spkeys[NHKF_GETPOS_SHOWVALID]) {
            if (getpos_hilitefunc) {
                getpos_toggle_hilite_state();
                curs(WIN_MAP, cx, cy);
            }
            show_goal_msg = TRUE; /* we're still targeting */
            goto nxtc;
        } else if (c == gc.Cmd.spkeys[NHKF_GETPOS_AUTODESC]) {
            iflags.autodescribe = !iflags.autodescribe;
            pline("Automatic description %sis %s.",
                  flags.verbose ? "of features under cursor " : "",
                  iflags.autodescribe ? "on" : "off");
            if (!iflags.autodescribe)
                show_goal_msg = TRUE;
            msg_given = TRUE;
            goto nxtc;
        } else if (c == gc.Cmd.spkeys[NHKF_GETPOS_LIMITVIEW]) {
            static const char *const view_filters[NUM_GFILTER] = {
                "Not limiting targets",
                "Limiting targets to those in sight",
                "Limiting targets to those in same area"
            };

            iflags.getloc_filter = (iflags.getloc_filter + 1) % NUM_GFILTER;
            for (i = 0; i < NUM_GLOCS; i++) {
                if (garr[i]) {
                    free((genericptr_t) garr[i]);
                    garr[i] = NULL;
                }
                gidx[i] = gcount[i] = 0;
            }
            pline("%s.", view_filters[iflags.getloc_filter]);
            msg_given = TRUE;
            goto nxtc;
        } else if (c == gc.Cmd.spkeys[NHKF_GETPOS_MENU]) {
            iflags.getloc_usemenu = !iflags.getloc_usemenu;
            pline("%s a menu to show possible targets%s.",
                  iflags.getloc_usemenu ? "Using" : "Not using",
                  iflags.getloc_usemenu
                      ? " for 'm|M', 'o|O', 'd|D', and 'x|X'" : "");
            msg_given = TRUE;
            goto nxtc;
        } else if (c == gc.Cmd.spkeys[NHKF_GETPOS_SELF]) {
            /* reset 'm&M', 'o&O', &c; otherwise, there's no way for player
               to achieve that except by manually cycling through all spots */
            for (i = 0; i < NUM_GLOCS; i++)
                gidx[i] = 0;
            cx = u.ux;
            cy = u.uy;
            goto nxtc;
        } else if (c == gc.Cmd.spkeys[NHKF_GETPOS_MOVESKIP]) {
            iflags.getloc_moveskip = !iflags.getloc_moveskip;
            pline("%skipping over similar terrain when fastmoving the cursor.",
                  iflags.getloc_moveskip ? "S" : "Not s");
            msg_given = TRUE;
            goto nxtc;
        } else if ((cp = strchr(mMoOdDxX, c)) != 0) { /* 'm|M', 'o|O', &c */
            /* nearest or farthest monster or object or door or unexplored */
            int gtmp = (int) (cp - mMoOdDxX), /* 0..7 */
                gloc = gtmp >> 1;             /* 0..3 */

            if (iflags.getloc_usemenu) {
                coord tmpcrd;

                if (getpos_menu(&tmpcrd, gloc)) {
                    cx = tmpcrd.x;
                    cy = tmpcrd.y;
                }
                goto nxtc;
            }

            if (!garr[gloc]) {
                gather_locs(&garr[gloc], &gcount[gloc], gloc);
                gidx[gloc] = 0; /* garr[][0] is hero's spot */
            }
            if (!(gtmp & 1)) {  /* c=='m' || c=='o' || c=='d' || c=='x') */
                gidx[gloc] = (gidx[gloc] + 1) % gcount[gloc];
            } else {            /* c=='M' || c=='O' || c=='D' || c=='X') */
                if (--gidx[gloc] < 0)
                    gidx[gloc] = gcount[gloc] - 1;
            }
            cx = garr[gloc][gidx[gloc]].x;
            cy = garr[gloc][gidx[gloc]].y;
            goto nxtc;
        } else {
            if (!strchr(quitchars, c)) {
                char matching[MAXPCHARS];
                int pass, k = 0;
                coordxy lo_x, lo_y, hi_x, hi_y;

                (void) memset((genericptr_t) matching, 0, sizeof matching);
                for (sidx = 0; sidx < MAXPCHARS; sidx++) {
                    /* don't even try to match some terrain: walls, room... */
                    if (is_cmap_wall(sidx) || is_cmap_room(sidx)
                        || is_cmap_corr(sidx) || is_cmap_door(sidx)
                        || sidx == S_ndoor)
                        continue;
                    if (c == defsyms[sidx].sym
                        || c == (int) gs.showsyms[sidx]
                        /* have '^' match webs and vibrating square or any
                           other trap that uses something other than '^' */
                        || (c == '^' && is_cmap_trap(sidx))
                        /* have room engraving character (default '`')
                           match corridor engravings (default '#') too */
                        || (c == gs.showsyms[S_engroom]
                            && is_cmap_engraving(sidx)))
                        matching[sidx] = (char) ++k;
                }
                if (k) {
                    for (pass = 0; pass <= 1; pass++) {
                        /* pass 0: just past current pos to lower right;
                           pass 1: upper left corner to current pos */
                        lo_y = (pass == 0) ? cy : 0;
                        hi_y = (pass == 0) ? ROWNO - 1 : cy;
                        for (ty = lo_y; ty <= hi_y; ty++) {
                            lo_x = (pass == 0 && ty == lo_y) ? cx + 1 : 1;
                            hi_x = (pass == 1 && ty == hi_y) ? cx : COLNO - 1;
                            for (tx = lo_x; tx <= hi_x; tx++) {
                                /* first, look at what is currently visible
                                   (might be monster) */
                                k = glyph_at(tx, ty);
                                if (glyph_is_cmap(k)
                                    && matching[glyph_to_cmap(k)])
                                    goto foundc;
                                /* next, try glyph that's remembered here
                                   (might be trap or object) */
                                if (svl.level.flags.hero_memory
                                    /* !terrainmode: don't move to remembered
                                       trap or object if not currently shown */
                                    && !iflags.terrainmode) {
                                    k = levl[tx][ty].glyph;
                                    if (glyph_is_cmap(k)
                                        && matching[glyph_to_cmap(k)])
                                        goto foundc;
                                }
                                /* FIXME: check player-specified vib.sq trap
                                   symbol rather than or in addition to '~' */
                                if (c == '~'
                                    && known_vibrating_square_at(tx, ty))
                                    goto foundc;
                                /* last, try actual terrain here (shouldn't
                                   we be using svl.lastseentyp[][] instead?) */
                                if (levl[tx][ty].seenv) {
                                    k = back_to_glyph(tx, ty);
                                    if (glyph_is_cmap(k)
                                        && matching[glyph_to_cmap(k)])
                                        goto foundc;
                                }
                                continue;
 foundc:
                                cx = tx, cy = ty;
                                if (msg_given) {
                                    clear_nhwindow(WIN_MESSAGE);
                                    msg_given = FALSE;
                                }
                                goto nxtc;
                            } /* column */
                        }     /* row */
                    }         /* pass */
                    pline("Can't find dungeon feature '%c'.", c);
                    msg_given = TRUE;
                    goto nxtc;
                } else {
                    char note[QBUFSZ];

                    if (!force)
                        Strcpy(note, "aborted");
                    else /* hjkl */
                        Sprintf(note, "use '%s', '%s', '%s', '%s' or '%s'",
                                visctrl(cmd_from_func(do_move_west)),
                                visctrl(cmd_from_func(do_move_south)),
                                visctrl(cmd_from_func(do_move_north)),
                                visctrl(cmd_from_func(do_move_east)),
                                visctrl(gc.Cmd.spkeys[NHKF_GETPOS_PICK]));
                    pline("Unknown direction: '%s' (%s).", visctrl((char) c),
                          note);
                    msg_given = TRUE;
                } /* k => matching */
            }     /* !quitchars */
            if (force)
                goto nxtc;
            pline("Done.");
            msg_given = FALSE; /* suppress clear */
            cx = -1;
            cy = 0;
            result = 0; /* not -1 */
            break;
        }
 nxtc:
        gg.getposx = cx, gg.getposy = cy;
#ifdef CLIPPING
        cliparound(cx, cy);
#endif
        curs(WIN_MAP, cx, cy);
        flush_screen(0);
    }
 exitgetpos:
#ifdef MACOS9
    lock_mouse_cursor(FALSE);
#endif
    lock_mouse_buttons(FALSE);
    if (msg_given)
        clear_nhwindow(WIN_MESSAGE);
    ccp->x = cx;
    ccp->y = cy;
    gg.getposx = gg.getposy = 0;
    for (i = 0; i < NUM_GLOCS; i++)
        if (garr[i])
            free((genericptr_t) garr[i]);
    getpos_sethilite(NULL, NULL);
    u.dx = udx, u.dy = udy, u.dz = udz;
    return result;
}

/*getpos.c*/
