Logo Search packages:      
Sourcecode: xconq version File versions  Download package

nlang.c

/* Interface-independent natural language handling for Xconq.
   Copyright (C) 1987-1989, 1991-2000 Stanley T. Shebs.

Xconq is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2, or (at your option)
any later version.  See the file COPYING.  */

/* This file should be entirely replaced for non-English Xconq. */
/* (One way to do this would be to call this file "nlang-en.c", then
   symlink this or nlang-fr.c, etc to nlang.c when configuring;
   similarly for help.c.) */
/* (A better way to would be to run all these through a dispatch
   vector, then each player could get messages and such in a
   different language.) */

#include "conq.h"
#include "kernel.h"

static void notify_combat(Unit *unit, Unit *atker, char *str);
static int pattern_matches_combat(Obj *pattern, Unit *unit, Unit *unit2);
static void combat_desc_from_list(Side *side, Obj *lis, Unit *unit,
                          Unit *unit2, char *str, char *buf);
static void init_calendar(void);
static void parse_date_step_range(Obj *form);
static void maybe_mention_date(Side *side);
static int gain_count(Side *side, int u, int r);
static int loss_count(Side *side, int u, int r);
static int atkstats(Side *side, int a, int d);
static int hitstats(Side *side, int a, int d);
static void pad_out(char *buf, int n);
static char *past_unit_handle(Side *side, PastUnit *past_unit);
static char *short_side_title_with_adjective(Side *side, char *adjective);

static char *tmpnbuf;

static char *tmpdbuf;

static char *pluralbuf;

/* Short names of directions. */

char *dirnames[] = DIRNAMES;

char *unitbuf = NULL;

char *past_unitbuf = NULL;

static char *side_short_title = NULL;

static char *gain_reason_names[] = { "Ini", "Bld", "Cap", "Oth" };

static char *loss_reason_names[] = { "Cbt", "Cap", "Stv", "Acc", "Dis", "Oth" };

/* Calendar handling. */

typedef enum {
    cal_unknown,
    cal_number,
    cal_usual
} CalendarType;

static CalendarType calendar_type = cal_unknown;

typedef enum {
    ds_second,
    ds_minute,
    ds_hour,
    ds_day,
    ds_week,
    ds_month,
    ds_season,
    ds_year
} UsualDateStepType;

typedef struct a_usualdate {
    int second;
    int minute;
    int hour;
    int day;
    int month;
    int year;
} UsualDate;

typedef struct a_usualdatesteprange {
    int turn_start;
    int turn_end;
    UsualDateStepType step_type;
    int step_size;
} UsualDateStepRange;

static char *usual_date_string(int date);
static void parse_usual_date(char *datestr, int range, UsualDate *udate);

static int turn_initial;

static UsualDateStepRange date_step_ranges[20];

static int num_date_step_ranges;

static char *months[] = {
  "Jan", "Feb", "Mar", "Apr", "May", "Jun",
  "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", "???" };

static short monthdays[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 0 };

static char *seasons[] = { "Win", "Spr", "Sum", "Aut" };

static UsualDate *usual_initial;

char *datebuf;

char *turn_name;

char *featurebuf;

/* This is the number of types to mention by name; any others will
   just be included in the count of missing images. */

#define NUMTOLIST 5

static char *missinglist;

/* This array allows for counting up to 4 classes of missing images. */

static int missing[4];

static int totlisted = 0;

/* Initialize things.  Note that this happens before any game is loaded, so
   can't do game-specific init here. */

void
init_nlang(void)
{
    if (tmpnbuf == NULL)
      tmpnbuf = xmalloc(BUFSIZE);
    if (tmpdbuf == NULL)
      tmpdbuf = xmalloc(BUFSIZE);
    if (pluralbuf == NULL)
      pluralbuf = xmalloc(BUFSIZE);
    if (datebuf == NULL)
      datebuf = xmalloc(BUFSIZE);
}

/* Send a message to everybody who's got a screen. */

void
notify_all(char *fmt, ...)
{
    va_list ap;
    Side *side;

    for_all_sides(side) {
      if (active_display(side)) {
          maybe_mention_date(side);
          va_start(ap, fmt);
          vsprintf(tmpnbuf, fmt, ap);
          va_end(ap);
          /* Always capitalize first char of notice. */
          capitalize(tmpnbuf);
          low_notify(side, tmpnbuf);
      }
    }
}

void
notify(Side *side, char *fmt, ...)
{
    va_list ap;

    if (!active_display(side))
      return;
    maybe_mention_date(side);
    va_start(ap, fmt);
    vsprintf(tmpnbuf, fmt, ap);
    va_end(ap);
    /* Always capitalize first char of notice. */
    capitalize(tmpnbuf);
    low_notify(side, tmpnbuf);
}

void
vnotify(Side *side, char *fmt, va_list ap)
{
    if (!active_display(side))
      return;
    maybe_mention_date(side);
    vsprintf(tmpnbuf, fmt, ap);
    /* Always capitalize first char of notice. */
    capitalize(tmpnbuf);
    low_notify(side, tmpnbuf);
}

static void
maybe_mention_date(Side *side)
{
    /* Note that last_notice_date defaults to 0, so this means that
       any turn 0 notices will not have a date prepended (which is good). */
    if (g_turn() != side->last_notice_date) {
      sprintf(tmpdbuf, "%s:", absolute_date_string(g_turn()));
      low_notify(side, tmpdbuf);
      side->last_notice_date = g_turn();
    }
}

/* Pad a given buffer with blanks out to the given position, and cut
   off any additional content. */

void
pad_out(char *buf, int n)
{
    int i, len = strlen(buf);

    if (n < 1)
      return;
    for (i = len; i < n; ++i) {
      buf[i] = ' ';
    }
    buf[n - 1] = '\0';
}

/* Get a char string naming the side.  Doesn't have to be pretty. */

/* (should synth complete name/adjective from other parts of speech) */

char *
side_name(Side *side)
{
    return (side->name ? side->name :
          (side->adjective ? side->adjective :
           (side->pluralnoun ? side->pluralnoun :
            (side->noun ? side->noun :
             ""))));
}

char *
side_adjective(Side *side)
{
    return (side->adjective ? side->adjective :
          (side->noun ? side->noun :
           (side->pluralnoun ? side->pluralnoun :
            (side->name ? side->name :
             ""))));
}

char *
short_side_title(Side *side)
{
    if (side_short_title == NULL)
      side_short_title = xmalloc(BUFSIZE);
    if (side->name) {
      return side->name;
    } else if (side->pluralnoun) {
      sprintf(side_short_title, "the %s", side->pluralnoun);
    } else if (side->noun) {
      sprintf(side_short_title, "the %s", plural_form(side->noun));
    } else if (side->adjective) {
      sprintf(side_short_title, "the %s side", side->adjective);
    } else {
      return " - ";
    }
    return side_short_title;
}

char *
short_side_title_with_adjective(Side *side, char *adjective)
{
    if (side_short_title == NULL)
      side_short_title = xmalloc(BUFSIZE);
    side_short_title[0] = '\0';
    if (side->name) {
      if (empty_string(adjective))
        return side->name;
      else {
          strcat(side_short_title, adjective);
          strcat(side_short_title, " ");
          strcat(side_short_title, side->name);
      }
    } else if (side->pluralnoun) {
      strcat(side_short_title, "the ");
      if (!empty_string(adjective)) {
          strcat(side_short_title, adjective);
          strcat(side_short_title, " ");
      }
      strcat(side_short_title, side->pluralnoun);
    } else if (side->noun) {
      strcat(side_short_title, "the ");
      if (!empty_string(adjective)) {
          strcat(side_short_title, adjective);
          strcat(side_short_title, " ");
      }
      strcat(side_short_title, side->noun);
    } else if (side->adjective) {
      strcat(side_short_title, "the ");
      if (!empty_string(adjective)) {
          strcat(side_short_title, adjective);
          strcat(side_short_title, " ");
      }
      strcat(side_short_title, side->adjective);
      strcat(side_short_title, " side");
    } else {
      return " - ";
    }
    return side_short_title;
}

/* This indicates whether the above routine returns a singular or plural form
   of title. */

int
short_side_title_plural_p(Side *side)
{
    if (side->name) {
      return FALSE;
    } else if (side->pluralnoun) {
      return TRUE;
    } else if (side->noun) {
      return TRUE;
    } else if (side->adjective) {
      sprintf(side_short_title, "the %s side", side->adjective);
      return FALSE;
    } else {
      return FALSE;
    }
}

char *
shortest_side_title(Side *side2, char *buf)
{
    if (side2->name) {
      return side2->name;
    } else if (side2->adjective) {
      return side2->adjective;
    } else if (side2->noun) {
      return side2->noun;
    } else if (side2->pluralnoun) {
      return side2->pluralnoun;
    } else {
      sprintf(buf, "(#%d)", side_number(side2));
    }
    return buf;
}

char *
sidemask_desc(char *buf, SideMask sidemask)
{
    int first = TRUE;
    Side *side2;

    if (sidemask < 0 || sidemask >= ((1 << (numsides + 1)) - 2))
      return "all";
    buf[0] = '\0';
    for_all_sides(side2) {
      if (side_in_set(side2, sidemask)) {
          if (first)
            first = FALSE;
          else
            strcat(buf, ", ");
          strcat(buf, short_side_title(side2));
      }
    }
    return buf;
}

char *
side_score_desc(char *buf, Side *side, Scorekeeper *sk)
{
    if (symbolp(sk->body)
            && (match_keyword(sk->body, K_LAST_SIDE_WINS)
                || match_keyword(sk->body, K_LAST_ALLIANCE_WINS))) {
      sprintf(buf, "Point Value: %d", side_point_value(side));
    } else {
      /* Compose the generic scorekeeper status display. */
      if (sk->title != NULL) {
          strcpy(buf, sk->title);
      } else {
          sprintf(buf, "SK #%d", sk->id);
      }
      if (sk->scorenum >= 0) {
          tprintf(buf, ": %d", side->scores[sk->scorenum]);
      }
    }
    return buf;
}

char *
long_player_title(char *buf, Player *player, char *thisdisplayname)
{
    buf[0] = '\0';
    if (player == NULL) {
      /* Do nothing */
    } else if (player->displayname != NULL) {
      if (player->name != NULL) {
          strcat(buf, player->name);
          strcat(buf, "@");
      }
      if (thisdisplayname != NULL
          && strcmp(player->displayname, thisdisplayname) == 0
          && player->rid == my_rid) {
          strcat(buf, "You");
      } else {
          strcat(buf, player->displayname);
      }
      if (player->aitypename != NULL) {
          strcat(buf, "(& AI ");
          strcat(buf, player->aitypename);
          strcat(buf, ")");
      }
    } else if (player->aitypename != NULL) {
      strcat(buf, "AI ");
      strcat(buf, player->aitypename);
    } else {
      strcat(buf, "-");
    }
    return buf;
}

char *
short_player_title(char *buf, Player *player, char *thisdisplayname)
{
    buf[0] = '\0';
    if (player == NULL)
      return buf;
    if (player->name != NULL) {
      strcat(buf, player->name);
    }
    if (player->aitypename != NULL) {
      strcat(buf, ",");
      strcat(buf, player->aitypename);
    }
    if ((player->name != NULL || player->aitypename != NULL)
      && player->displayname != NULL) {
      strcat(buf, "@");
    }
    if (thisdisplayname != NULL
      && player->displayname != NULL
      && strcmp(player->displayname, thisdisplayname) == 0) {
      strcat(buf, "You");
    } else if (player->displayname != NULL) {
      strcat(buf, player->displayname);
    }
    if (strlen(buf) == 0) {
      strcat(buf, "-");
    }
    return buf;
}

#if 0       /* Unused. */

void
side_and_type_name(char *buf, Side *side, int u, Side *side2)
{
    /* Decide how to identify the side. */
    if (side2 == NULL) {
      sprintf(buf, "independent ");
    } else if (side == side2) {
      sprintf(buf, "your ");
    } else {
      sprintf(buf, "%s ", side_adjective(side2));
    }
    /* Glue the pieces together and return it. */
    strcat(buf, u_type_name(u));
}

#endif

/* Build a short phrase describing a given unit to a given side,
   basically consisting of indication of unit's side, then of unit
   itself. */

char *
unit_handle(Side *side, Unit *unit)
{
    Side *side2 = NULL;

    if (unit != NULL)
      side2 = unit->side;
    return apparent_unit_handle(side, unit, side2);
}

/* This version allows the caller to supply a side other than the
   unit's actual side, such as when describing an out-of-date image of
   a unit that may have been captured. */

char *
apparent_unit_handle(Side *side, Unit *unit, Side *side2)
{
    char *utypename, *fmtstr, smallbuf[40], sidebuf[100];
    Side *side3;
    Obj *frest, *fmt1;

    /* This should be impossible, be really obvious if it happens. */
    if (side2 == NULL)
      return "null side2 in apparent_unit_handle?";
    /* Make sure our working space exists. */
    if (unitbuf == NULL)
      unitbuf = xmalloc(BUFSIZE);
    /* Handle various weird situations. */
    if (unit == NULL)
      return "???";
    if (!alive(unit)) {
      sprintf(unitbuf, "dead #%d", unit->id);
        return unitbuf;
    }
    /* If this unit represents "yourself", say so. */
    if (side != NULL && unit == side->self_unit)
      return "you";
    unitbuf[0] = '\0';
    /* Decide how to identify the side.  If the unit's original side
       is not its current side, list both of them. */
    side3 = NULL;
    if (unit->origside != NULL && side2 != unit->origside)
      side3 = unit->origside;
    sidebuf[0] = '\0';
    if (side2 == side) {
      strcat(sidebuf, "your");
    } else {
      /* If the side adjective is a genitive (ends in 's, s' or z')
         we should skip the definite article. */
      int len = strlen(side_adjective(side2));
      char *end = side_adjective(side2) + len - 2;

      if (strcmp(end, "'s") != 0
          && strcmp(end, "s'") != 0
          && strcmp(end, "z'") != 0)
        strcat(sidebuf, "the ");
      strcat(sidebuf, side_adjective(side2));
    }
    if (side3 != NULL) {
      if (side3 == side) {
          strcat(sidebuf, " (formerly your)");
      } else if (side3 == indepside) {
          /* Don't add anything for captured independents.  While
             technically there's no reason not to do this, in
             practice a side will often have many captured indeps,
             and this keeps the text from getting too cluttered. */
      } else {
          strcat(sidebuf, " (formerly ");
          strcat(sidebuf, side_adjective(side3));
          strcat(sidebuf, ")");
      }
    }
    /* Now add the unit's unique description. */
    utypename = u_type_name(unit->type);
    /* If we have special formatting info, interpret it. */
    if (u_desc_format(unit->type) != lispnil) {
      for_all_list(u_desc_format(unit->type), frest) {
          fmt1 = car(frest);
          if (stringp(fmt1)) {
            /* Append strings verbatim. */
            strcat(unitbuf, c_string(fmt1));
          } else if (symbolp(fmt1)) {
            /* Symbols indicate the types of data to format and
                   output. */
            fmtstr = c_string(fmt1);
            if (strcmp(fmtstr, "name") == 0) {
                strcat(unitbuf, (unit->name ? unit->name : "anon"));
            } else if (strcmp(fmtstr, "position") == 0) {
                sprintf(smallbuf, "%d,%d", unit->x, unit->y);
                strcat(unitbuf, smallbuf);
            } else if (strcmp(fmtstr, "side") == 0) {
                strcat(unitbuf, sidebuf);
            } else if (strcmp(fmtstr, "type") == 0) {
                strcat(unitbuf, utypename);
            } else if (strcmp(fmtstr, "side-name") == 0) {
                strcat(unitbuf, side_name(unit->side));
            } else if (strcmp(fmtstr, "side-adjective") == 0) {
                strcat(unitbuf, side_adjective(unit->side));
            } else {
                strcat(unitbuf, "??description-format??");
            }
          } else {
            strcat(unitbuf, "??description-format??");
          }
      }
      return unitbuf;
    } else {
      strcat(unitbuf, sidebuf);
      strcat(unitbuf, " ");
    }
    /* Default formats for units. */
    if (unit->name) {
      tprintf(unitbuf, "%s %s", utypename, unit->name);
    } else if (unit->number > 0) {
      tprintf(unitbuf, "%d%s %s",
            unit->number, ordinal_suffix(unit->number), utypename);
    } else {
      strcat(unitbuf, utypename);
    }
    return unitbuf;
}

/* Shorter unit description omits side name, but uses same buffer.
   This is mainly useful for describing the transport of a unit and
   suchlike, where the player will likely already know the side of the
   unit. */

char *
short_unit_handle(Unit *unit)
{
    int u;

    if (unitbuf == NULL)
      unitbuf = xmalloc(BUFSIZE);
    if (unit == NULL)
      return "???";
    if (!alive(unit)) {
      sprintf(unitbuf, "dead #%d", unit->id);
        return unitbuf;
    }
    /* If this unit represents "yourself", say so. */
    if (unit->side != NULL && unit == unit->side->self_unit)
      return "you";
    u = unit->type;
    /* Use the name alone if the unit is named, or else use optional
       ordinal and the shortest type name available. */
    if (!empty_string(unit->name)) {
      strcpy(unitbuf, unit->name);
    } else {
      unitbuf[0] = '\0';
      if (unit->number > 0) {
          sprintf(unitbuf, "%d%s ",
                unit->number, ordinal_suffix(unit->number));
      }
      if (!empty_string(u_short_name(u)))
        strcat(unitbuf, u_short_name(u));
      else
        strcat(unitbuf, u_type_name(u));
    }
    return unitbuf;
}

/* This version lists the side but skips original side etc. */

char *
medium_long_unit_handle(Unit *unit)
{
    if (unitbuf == NULL)
      unitbuf = xmalloc(BUFSIZE);
    if (unit == NULL)
      return "???";
    if (!alive(unit)) {
      sprintf(unitbuf, "dead #%d", unit->id);
        return unitbuf;
    }
    strcpy(unitbuf, side_adjective(unit->side));
    /* If the unit has a name, write its type followed by the name. */
    if (!empty_string(unit->name)) {
      strcat(unitbuf, " ");
      strcat(unitbuf, u_type_name(unit->type));
      strcat(unitbuf, " ");
      strcat(unitbuf, unit->name);
    /* If the unit has a number, write it followed by the type. */
    } else if (unit->number > 0) {
      tprintf(unitbuf, " %d%s %s",
            unit->number, ordinal_suffix(unit->number),
            u_type_name(unit->type));
    /* Else just write the unit type. */
    } else {
      strcat(unitbuf, " ");
      strcat(unitbuf, u_type_name(unit->type));
    }
    return unitbuf;
}

/* Put either the unit's name or its number into the given buffer. */

void
name_or_number(Unit *unit, char *buf)
{
    if (unit->name) {
      strcpy(buf, unit->name);
    } else if (unit->number > 0) {
      sprintf(buf, "%d%s", unit->number, ordinal_suffix(unit->number));
    } else {
      buf[0] = '\0';
    }
}

/* Build a short phrase describing a given past unit to a given side,
   basically consisting of indication of unit's side, then of unit
   itself. */

char *
past_unit_handle(Side *side, PastUnit *past_unit)
{
    char *utypename;
    Side *side2;

    if (past_unitbuf == NULL)
      past_unitbuf = xmalloc(BUFSIZE);
    /* Handle various weird situations. */
    if (past_unit == NULL)
      return "???";
    /* Decide how to identify the side. */
    side2 = past_unit->side;
    if (side2 == NULL) {
      sprintf(past_unitbuf, "the ");
    } else if (side2 == side) {
      sprintf(past_unitbuf, "your ");
    } else {
      /* If the side adjective is a genitive (ends in 's, s' or z')
         we should skip the definite article. */
      int len = strlen(side_adjective(side2));
      char *end = side_adjective(side2) + len - 2;

      if (strcmp(end, "'s") != 0
          && strcmp(end, "s'") != 0
          && strcmp(end, "z'") != 0)
        sprintf(past_unitbuf, "the ");
      sprintf(past_unitbuf, side_adjective(side2));
    }
    /* Now add the past_unit's unique description. */
    utypename = u_type_name(past_unit->type);
    if (past_unit->name) {
      tprintf(past_unitbuf, "%s %s", utypename, past_unit->name);
    } else if (past_unit->number > 0) {
      tprintf(past_unitbuf, "%d%s %s",
            past_unit->number, ordinal_suffix(past_unit->number),
            utypename);
    } else {
      strcat(past_unitbuf, utypename);
    }
    return past_unitbuf;
}

/* Given a unit and optional type u, summarize construction status
   and timing. */

void
construction_desc(char *buf, Unit *unit, int u)
{
    int est, u2;
    char ubuf[10], tmpbuf[100];
    Task *task;
    Unit *unit2;

    if (u != NONUTYPE) {
      est = est_completion_time(unit, u);
      if (est >= 0) {
          sprintf(ubuf, "[%2d] ", est);
      } else {
          strcpy(ubuf, " --  ");
      }
    } else {
      ubuf[0] = '\0';
    }
    name_or_number(unit, tmpbuf);
    sprintf(buf, "%s%s %s", ubuf, u_type_name(unit->type), tmpbuf);
    pad_out(buf, 25);
    if (unit->plan
      && unit->plan->tasks) {
      task = unit->plan->tasks;
      if (task->type == TASK_BUILD) {
          u2 = task->args[0];
          tprintf(buf, " %s ", (is_unit_type(u2) ? u_type_name(u2) : "?"));
          unit2 = find_unit(task->args[1]);
          if (in_play(unit2) && unit2->type == u2) {
            tprintf(buf, "%d/%d done ", unit2->cp, u_cp(unit2->type));
          }
          tprintf(buf, "(%d of %d)", task->args[2] + 1, task->args[3]);
      } else if (task->type == TASK_DEVELOP) {
          u2 = task->args[0];
          if (is_unit_type(u2)) {
            tprintf(buf, " %s tech %d/%d",
                  u_type_name(u2), unit->side->tech[u2], task->args[1]);
          }
      }
    }
}

/* Given a unit and optional advance a, summarize research status
   and timing. */

void
research_desc(char *buf, Unit *unit, int a)
{
    char abuf[10], tmpbuf[100];

    if (a != NONATYPE) {
      if (u_can_research(unit->type)) {
          sprintf(abuf, "[%2d] ", a_rp(a));
      } else {
          strcpy(abuf, " --  ");
      }
    } else {
      abuf[0] = '\0';
    }
    name_or_number(unit, tmpbuf);
    sprintf(buf, "%s%s %s", abuf, u_type_name(unit->type), tmpbuf);
    pad_out(buf, 25);
}

void
researchible_desc(char *buf, Unit *unit, int a)
{
    char abuf[10];

    if (a != NONATYPE && unit != NULL) {
      if (u_advanced(unit->type)) {
          sprintf(abuf, "[%2d] ", a_rp(a));
      } else {
          strcpy(abuf, " --  ");
      }
    } else {
      abuf[0] = '\0';
    }
    sprintf(buf, "%s%s", abuf, a_type_name(a));
    pad_out(buf, 25);
}

/* This generates a textual description of a type's construction info,
   including estimated time for the given unit to build one, tooling &
   tech, plus number of that type in existence already. */

void
constructible_desc(char *buf, Side *side, int u, Unit *unit)
{
    char estbuf[20];
    char techbuf[50];
    char typenamebuf[50];
    int est, tp, num;

    if (unit != NULL) {
      est = est_completion_time(unit, u);
      if (est >= 0) {
          sprintf(estbuf, "[%2d] ", est);
          if (uu_tp_to_build(unit->type, u) > 0) {
            tp = (unit->tooling ? unit->tooling[u] : 0);
            tprintf(estbuf, "(%2d) ", tp);
          }
      } else {
          strcpy(estbuf, " --  ");
      }
    } else {
      estbuf[0] = '\0';
    }
    if (u_tech_max(u) > 0) {
      sprintf(techbuf, "[Tech %d/%d/%d] ",
            side->tech[u], u_tech_to_build(u), u_tech_max(u));
    } else {
      techbuf[0] = '\0';
    }
    strcpy(typenamebuf, u_type_name(u));
    /* If the single char for the type is different from the first character
       of its type name, mention the char. */
    if (!empty_string(u_uchar(u)) && (u_uchar(u))[0] != typenamebuf[0]) {
      tprintf(typenamebuf, "(%c)", (u_uchar(u))[0]);
    }
    sprintf(buf, "%s%s%-16.16s", estbuf, techbuf, typenamebuf);
    num = num_units_in_play(side, u);
    if (num > 0) {
      tprintf(buf, "  %3d", num);
    } else {
      strcat(buf, "     ");
    }
    num = num_units_incomplete(side, u);
    if (num > 0) {
      tprintf(buf, "(%d)", num);
    }
}

#if 0       /* Unused. */

void
historical_event_date_desc(HistEvent *hevt, char *buf)
{
    sprintf(buf, "%d: ", hevt->startdate);
}

#endif

int
find_event_type(Obj *sym)
{
    int i;

    for (i = 0; hevtdefns[i].name != NULL; ++i) {
      if (strcmp(c_string(sym), hevtdefns[i].name) == 0)
        return i;
    }
    return -1;
}

/* (should abstract out evt arg -> unit/pastunit description code) */

void
historical_event_desc(Side *side, HistEvent *hevt, char *buf)
{
    int data0 = hevt->data[0];
    int data1 = hevt->data[1];
    Obj *rest, *head, *pattern, *text;
    Unit *unit;
    PastUnit *pastunit, *pastunit2;
    Side *side2;
    
    for_all_list(g_event_narratives(), rest) {
      head = car(rest);
      if (consp(head)) {
          pattern = car(head);
          if (symbolp(pattern)
            && find_event_type(pattern) == hevt->type) {
            text = cadr(head);
            if (stringp(text)) {
                sprintf(buf, c_string(text));
            } else {
                sprintlisp(buf, text, 50);
            }
            return;
          } else if (consp(pattern)
                   && symbolp(car(pattern))
                   && pattern_matches_event(pattern, hevt)
                   ) {
            text = cadr(head);
            if (stringp(text)) {
                sprintf(buf, c_string(text));
            } else {
                event_desc_from_list(side, text, hevt, buf);
            }
            return;
          }
      }
    }
    /* Generate a default description of the event. */
    switch (hevt->type) {
      case H_LOG_STARTED:
      sprintf(buf, "we started recording events");
      break;
      case H_LOG_ENDED:
      sprintf(buf, "we stopped recording events");
      break;
      case H_GAME_STARTED:
      sprintf(buf, "we started the game");
      break;
      case H_GAME_SAVED:
      sprintf(buf, "we saved the game");
      break;
      case H_GAME_RESTARTED:
      sprintf(buf, "we restarted the game");
      break;
      case H_GAME_ENDED:
      sprintf(buf, "we ended the game");
      break;
      case H_SIDE_JOINED:
            side2 = side_n(data0);
      sprintf(buf, "%s joined the game",
            (side == side2 ? "you" : side_name(side2)));
      break;
      case H_SIDE_LOST:
            side2 = side_n(data0);
      sprintf(buf, "%s lost!", (side == side2 ? "you" : side_name(side2)));
      /* Include an explanation of the cause, if there is one. */
      if (data1 == -1) {
          tprintf(buf, " (resigned)");
      } else if (data1 == -2) {
          tprintf(buf, " (self-unit died)");
      } else if (data1 > 0) {
          tprintf(buf, " (scorekeeper %d)", data1);
      } else {
          tprintf(buf, " (don't know why)");
      }
      break;
      case H_SIDE_WITHDREW:
            side2 = side_n(data0);
      sprintf(buf, "%s withdrew!", (side == side2 ? "you" : side_name(side2)));
      break;
      case H_SIDE_WON:
            side2 = side_n(data0);
      sprintf(buf, "%s won!", (side == side2 ? "you" : side_name(side2)));
      /* Include an explanation of the cause, if there is one. */
      if (data1 > 0) {
          tprintf(buf, " (scorekeeper %d)", data1);
      } else {
          tprintf(buf, " (don't know why)");
      }
      break;
      case H_UNIT_CREATED:
            side2 = side_n(data0);
      sprintf(buf, "%s created ",
            (side == side2 ? "you" : side_name(side2)));
      unit = find_unit(data1);
      if (unit != NULL) {
          strcat(buf, unit_handle(side, unit));
      } else {
          pastunit = find_past_unit(data1);
          if (pastunit != NULL) {
            strcat(buf, past_unit_handle(side, pastunit));
          } else {
            tprintf(buf, "%d??", data1);
          }
      }
      break;
      case H_UNIT_COMPLETED:
            side2 = side_n(data0);
      sprintf(buf, "%s completed ",
            (side == side2 ? "you" : side_name(side2)));
      unit = find_unit(data1);
      if (unit != NULL) {
          strcat(buf, unit_handle(side, unit));
      } else {
          pastunit = find_past_unit(data1);
          if (pastunit != NULL) {
            strcat(buf, past_unit_handle(side, pastunit));
          } else {
            tprintf(buf, "%d??", data1);
          }
      }
      break;
      case H_UNIT_DAMAGED:
      unit = find_unit_dead_or_alive(data0);
      if (unit != NULL) {
          strcpy(buf, unit_handle(side, unit));
      } else {
          pastunit = find_past_unit(data0);
          if (pastunit != NULL) {
            strcpy(buf, past_unit_handle(side, pastunit));
          } else {
            sprintf(buf, "%d??", data0);
          }
      }
      tprintf(buf, " damaged (%d -> %d hp)", data1, hevt->data[2]);
      break;
      case H_UNIT_CAPTURED:
      buf[0] = '\0';
      /* Note that the second optional value, if present, is the id
         of the unit that did the capturing. */
      unit = find_unit_dead_or_alive(data1);
      if (unit != NULL) {
          strcat(buf, unit_handle(side, unit));
      } else {
          pastunit = find_past_unit(data1);
          if (pastunit != NULL) {
            strcat(buf, past_unit_handle(side, pastunit));
          } else if (data1 == 0) {
            tprintf(buf, "somebody");
          } else {
            tprintf(buf, "%d??", data1);
          }
      }
      tprintf(buf, " captured ");
      /* Describe the unit that was captured. */
      unit = find_unit_dead_or_alive(data0);
      if (unit != NULL) {
          strcat(buf, unit_handle(side, unit));
      } else {
          pastunit = find_past_unit(data0);
          if (pastunit != NULL) {
            strcat(buf, past_unit_handle(side, pastunit));
          } else {
            tprintf(buf, "%d??", data0);
          }
      }
      break;
      case H_UNIT_SURRENDERED:
      buf[0] = '\0';
      /* Describe the unit that surrendered. */
      unit = find_unit_dead_or_alive(data0);
      if (unit != NULL) {
          strcat(buf, unit_handle(side, unit));
      } else {
          pastunit = find_past_unit(data0);
          if (pastunit != NULL) {
            strcat(buf, past_unit_handle(side, pastunit));
          } else {
            tprintf(buf, "%d??", data0);
          }
      }
      tprintf(buf, " surrendered to ");
      /* The second optional value, if present, is the id of the
         unit that accepted the surrender. */
      unit = find_unit_dead_or_alive(data1);
      if (unit != NULL) {
          strcat(buf, unit_handle(side, unit));
      } else {
          pastunit = find_past_unit(data1);
          if (pastunit != NULL) {
            strcat(buf, past_unit_handle(side, pastunit));
          } else if (data1 == 0) {
            tprintf(buf, "somebody");
          } else {
            tprintf(buf, "%d??", data1);
          }
      }
      break;
      case H_UNIT_ACQUIRED:
      buf[0] = '\0';
      if (data1 >= 0) {
          side2 = side_n(data1);
          strcat(buf, (side == side2 ? "you" : side_name(side2)));
      } else {
          tprintf(buf, "somebody");
      }
      tprintf(buf, " acquired ");
      /* Describe the unit that was acquired. */
      unit = find_unit_dead_or_alive(data0);
      if (unit != NULL) {
          strcat(buf, unit_handle(side, unit));
      } else {
          pastunit = find_past_unit(data0);
          if (pastunit != NULL) {
            strcat(buf, past_unit_handle(side, pastunit));
          } else {
            tprintf(buf, "%d??", data0);
          }
      }
      break;
      case H_UNIT_REVOLTED:
      buf[0] = '\0';
      /* Describe the unit that revolted. */
      unit = find_unit_dead_or_alive(data0);
      if (unit != NULL) {
          strcat(buf, unit_handle(side, unit));
      } else {
          pastunit = find_past_unit(data0);
          if (pastunit != NULL) {
            strcat(buf, past_unit_handle(side, pastunit));
          } else {
            tprintf(buf, "%d??", data0);
          }
      }
      tprintf(buf, " revolted");
      if (data1 >= 0) {
          side2 = side_n(data1);
          tprintf(buf, ", went over to %s", (side == side2 ? "you" : side_name(side2)));
      }
      break;
      case H_UNIT_KILLED:
      case H_UNIT_DIED_IN_ACCIDENT:
      case H_UNIT_DIED_FROM_TEMPERATURE:
      /* Obviously, the unit mentioned here can only be a past unit. */
      pastunit = find_past_unit(data0);
      if (pastunit != NULL) {
          sprintf(buf, "%s", past_unit_handle(side, pastunit));
      } else {
          sprintf(buf, "%d??", data0);
      }
      if (hevt->type == H_UNIT_KILLED)
        tprintf(buf, " was destroyed");
      else if (hevt->type == H_UNIT_DIED_IN_ACCIDENT)
        tprintf(buf, " died in an accident");
      else
        tprintf(buf, " died from excessive temperature");
      break;
      case H_UNIT_WRECKED:
      case H_UNIT_WRECKED_IN_ACCIDENT:
      pastunit = find_past_unit(data0);
      if (pastunit != NULL) {
          sprintf(buf, "%s", past_unit_handle(side, pastunit));
      } else {
          sprintf(buf, "%d??", data0);
      }
      if (hevt->type == H_UNIT_WRECKED)
        tprintf(buf, " was wrecked");
      else
        tprintf(buf, " was wrecked in an accident");
      break;
      case H_UNIT_VANISHED:
      pastunit = find_past_unit(data0);
      if (pastunit != NULL) {
          sprintf(buf, "%s", past_unit_handle(side, pastunit));
      } else {
          sprintf(buf, "%d??", data0);
      }
      tprintf(buf, " vanished");
      break;
      case H_UNIT_DISBANDED:
      pastunit = find_past_unit(data0);
      if (pastunit != NULL) {
          sprintf(buf, "%s", past_unit_handle(side, pastunit));
      } else {
          sprintf(buf, "%d??", data0);
      }
      tprintf(buf, " was disbanded");
      break;
      case H_UNIT_GARRISONED:
      pastunit = find_past_unit(data0);
      if (pastunit != NULL) {
          sprintf(buf, "%s", past_unit_handle(side, pastunit));
      } else {
          sprintf(buf, "%d??", data0);
      }
      tprintf(buf, " was used to garrison ");
      unit = find_unit(data1);
      if (unit != NULL) {
          strcat(buf, unit_handle(side, unit));
      } else {
          pastunit2 = find_past_unit(data1);
          if (pastunit2 != NULL) {
            strcat(buf, past_unit_handle(side, pastunit2));
          } else {
            /* Should never happen, but don't choke if it does. */
            tprintf(buf, "?????");
          }
      }
      break;
      case H_UNIT_STARVED:
      pastunit = find_past_unit(data0);
      if (pastunit != NULL) {
          sprintf(buf, "%s", past_unit_handle(side, pastunit));
      } else {
          sprintf(buf, "%d??", data0);
      }
      tprintf(buf, " starved to death");
      break;
      case H_UNIT_MERGED:
      pastunit = find_past_unit(data0);
      if (pastunit != NULL) {
          sprintf(buf, "%s", past_unit_handle(side, pastunit));
      } else {
          sprintf(buf, "%d??", data0);
      }
      tprintf(buf, " merged into another");
      break;
      case H_UNIT_LEFT_WORLD:
      pastunit = find_past_unit(data0);
      if (pastunit != NULL) {
          sprintf(buf, "%s", past_unit_handle(side, pastunit));
      } else {
          sprintf(buf, "%d??", data0);
      }
      tprintf(buf, " left the world");
      break;
      case H_UNIT_NAME_CHANGED:
      pastunit = find_past_unit(data0);
      if (pastunit != NULL) {
          sprintf(buf, "%s", past_unit_handle(side, pastunit));
      } else {
          sprintf(buf, "%d??", data0);
      }
      unit = find_unit(data1);
      if (unit != NULL) {
          if (unit->name != NULL)
            tprintf(buf, " changed name to \"%s\"", unit->name);
          else
            tprintf(buf, " became anonymous");
      } else {
          pastunit2 = find_past_unit(data1);
          if (pastunit2 != NULL) {
            if (pastunit2->name != NULL)
              tprintf(buf, " changed name to \"%s\"", pastunit2->name);
            else
              tprintf(buf, " became anonymous");
          } else
            tprintf(buf, " no name change???");
      }
      break;
      default:
      /* Don't warn, will cause serious problems for windows that
         display lists of events, but make sure the non-understood event
         is obvious. */
      sprintf(buf, "?????????? \"%s\" ??????????",
            hevtdefns[hevt->type].name);
      break;
    }
}

int
pattern_matches_event(Obj *pattern, HistEvent *hevt)
{
    int data0, u;
    Obj *rest, *subpat;
    PastUnit *pastunit;

    if (find_event_type(car(pattern)) != hevt->type)
      return FALSE;
    data0 = hevt->data[0];
    for_all_list(cdr(pattern), rest) {
      subpat = car(rest);
      if (symbolp(subpat)) {
          switch (hevt->type) {
            case H_UNIT_STARVED:
            u = utype_from_name(c_string(subpat));
            pastunit = find_past_unit(data0);
            if (pastunit != NULL && pastunit->type == u)
              return TRUE;
            else
              return FALSE;
            break;
            default:
            return FALSE;
          }
      } else {
          /* (should warn of bad pattern syntax?) */
          return FALSE;
      }
    }
    return TRUE;
}

void
event_desc_from_list(Side *side, Obj *lis, HistEvent *hevt, char *buf)
{
    int n;
    Obj *rest, *item;
    PastUnit *pastunit;

    buf[0] = '\0';
    for_all_list(lis, rest) {
      item = car(rest);
      if (stringp(item)) {
          strcat(buf, c_string(item));
      } else if (numberp(item)) {
          n = c_number(item);
          if (between(0, n, 3)) {
            switch (hevt->type) {
              case H_UNIT_STARVED:
                pastunit = find_past_unit(hevt->data[0]);
                if (pastunit != NULL) {
                  strcat(buf, past_unit_handle(side, pastunit));
                } else {
                  tprintf(buf, "%d?", hevt->data[0]);
                }
                break;
              /* (should add other event types) */
              default:
                break;
            }
          } else {
            tprintf(buf, " ??%d?? ", n);
          }
      } else {
          strcat(buf, " ????? ");
      }
    }
}

/* Return a string describing the action's result. */

char *
action_result_desc(int rslt)
{
    char *str;
    
    switch (rslt) {
      case A_ANY_OK:
      str = "OK";
      break;
      case A_ANY_DONE:
      str = "done";
      break;
      case A_ANY_CANNOT_DO:
      str = "can never do";
      break;
      case A_ANY_NO_ACP:
      str = "insufficient acp";
      break;
      case A_ANY_NO_MATERIAL:
      str = "insufficient material";
      break;
      case A_ANY_NO_AMMO:
      str = "insufficient ammo";
      break;
      case A_ANY_TOO_FAR:
      str = "too far";
      break;
      case A_ANY_TOO_NEAR:
      str = "too near";
      break;
      case A_MOVE_NO_MP:
      str = "insufficient mp";
      break;
      case A_MOVE_BLOCKING_ZOC:
      str = "blocking ZOC present";
      break;
      case A_MOVE_CANNOT_LEAVE_WORLD:
      str = "cannot leave world";
      break;
      case A_MOVE_DEST_FULL:
      str = "destination full";
      break;
      case A_OVERRUN_FAILED:
      str = "overrun failed";
      break;
      case A_OVERRUN_SUCCEEDED:
      str = "overrun succeeded";
      break;
      case A_FIRE_BLOCKED:
      str = "fire blocked by terrain";
      break;
      case A_FIRE_INTO_OUTSIDE_WORLD:
      str = "cannot fire outside world";
      break;
      case A_CAPTURE_FAILED:
      str = "capture failed";
      break;
      case A_CAPTURE_SUCCEEDED:
      str = "capture succeeded";
      break;
      case A_ANY_ERROR:
      str = "misc error";
      break;
      default:
      if (between(0, rslt, NUMHEVTTYPES - 1))
        str = hevtdefns[rslt].name;
      else
        /* This should never happen. */
        str = "???";
      break;
    }
    return str;
}

/* Explain the reason for failure to move into a cell. */

void
advance_failure_desc(char *buf, Unit *unit, HistEventType reason)
{
    strcpy(buf, unit_handle(unit->side, unit));
    if (reason == A_ANY_CANNOT_DO)
      strcat(buf, " can never do this!");
    else if (reason == A_ANY_NO_ACP)
      strcat(buf, " does not have enough ACP!");
    else if (reason == A_ANY_NO_MATERIAL)
      strcat(buf, " is out of some material!");
    else if (reason == A_ANY_NO_AMMO)
      strcat(buf, " is out of ammo!");
    else if (reason == A_MOVE_NO_MP)
      /* Player doesn't see mp calcs, so explain as ACP */
      strcat(buf, " does not have enough ACP!");
    else if (reason == A_MOVE_CANNOT_LEAVE_WORLD)
      strcat(buf, " can never leave the world!");
    else if (reason == A_MOVE_BLOCKING_ZOC)
      strcat(buf, " cannot enter a blocking ZOC!");
    else if (reason == A_MOVE_DEST_FULL)
      strcat(buf, " cannot fit into the destination!");
    else if (reason != H_UNDEFINED)
      sprintf(buf, " cannot act, reason is %s", action_result_desc(reason));
    else
      strcat(buf, " is unable to act, don't know why");
}

/* Generate a description of the borders and connections in and around
   a location. */

void
linear_desc(char *buf, int x, int y)
{
    int t, first = TRUE;

    buf[0] = '\0';
    if (any_aux_terrain_defined()) {
      for_all_terrain_types(t) {
          if (t_is_border(t)
            && aux_terrain_defined(t)
            && any_borders_at(x, y, t)) {

#if 0 /* Confusing. */
            if (first) {
                strcat(buf, " at ");
                first = FALSE;
            } else {
                strcat(buf, ",");
            }
            strcat(buf, t_type_name(t));
#endif

#if 0 /* takes up more space, but not very useful */
            for_all_directions(dir) {
                if (border_at(x, y, dir, t)) {
                  tprintf(buf, "/%s", dirnames[dir]);
                }
            }
#endif

          } else if (t_is_connection(t)
                   && aux_terrain_defined(t)
                   && any_connections_at(x, y, t)) {
            if (first) {
                strcat(buf, " + ");
                first = FALSE;
            } else {
                strcat(buf, ",");
            }
            strcat(buf, t_type_name(t));
#if 0
            for_all_directions(dir) {
                if (connection_at(x, y, dir, t)) {
                  tprintf(buf, "/%s", dirnames[dir]);
                }
            }
#endif
          }
      }
    }
}

void
elevation_desc(char *buf, int x, int y)
{
    if (elevations_defined()) {
      sprintf(buf, "(Elev %d)", elev_at(x, y));
    }
}

char *
feature_desc(Feature *feature, char *buf)
{
    int i, caps = FALSE;
    char *str;

    if (feature == NULL)
      return NULL;
    if (feature->name) {
      /* Does the name need any substitutions done? */
      if (strchr(feature->name, '%')) {
          i = 0;
          for (str = feature->name; *str != '\0'; ++str) {
            if (*str == '%') {
                /* Interpret substitution directives. */
                switch (*(str + 1)) {
                  case 'T':
                  caps = TRUE;
                  case 't':
                  if (feature->typename) {
                      buf[i] = '\0';
                      strcat(buf, feature->typename);
                      if (caps)
                        capitalize(buf);
                      i = strlen(buf);
                  }
                  ++str;
                  break;
                  default:
                  break;
                }
            } else {
                buf[i++] = *str;
            }
          }
          /* Close off the string. */
          buf[i] = '\0';
          return buf;
      } else {
          /* Return the name alone. */
          return feature->name;
      }
    } else {
      if (feature->typename) {
          strcpy(buf, "unnamed ");
          strcat(buf, feature->typename);
          return buf;
      }
    }
    /* No description of the location is available. */
    return "anonymous feature";
}

/* Generate a string describing what is at the given location. */

char *
feature_name_at(int x, int y)
{
    int fid = (features_defined() ? raw_feature_at(x, y) : 0);
    Feature *feature;

    if (fid == 0)
      return NULL;
    feature = find_feature(fid);
    if (feature != NULL) {
      if (featurebuf == NULL)
        featurebuf = xmalloc(BUFSIZE);
      return feature_desc(feature, featurebuf);
    }
    /* No description of the location is available. */
    return NULL;
}

void
temperature_desc(char *buf, int x, int y)
{
    if (temperatures_defined()) {
      sprintf(buf, "(Temp %d)", temperature_at(x, y));
    }
}

#if 0
    int age, u;
    short view, prevview;
    Side *side2;

    /* Compose and display view history of this cell. */
    Dprintf("Drawing previous view info\n");
    age = side_view_age(side, curx, cury);
    prevview = side_prevview(side, curx, cury);
    if (age == 0) {
      if (prevview != view) {
          if (prevview == EMPTY) {
            /* misleading if prevview was set during init. */
            sprintf(tmpbuf, "Up to date; had been empty.");
          } else if (prevview == UNSEEN) {
            sprintf(tmpbuf, "Up to date; had been unexplored.");
          } else {
            side2 = side_n(vside(prevview));
            u = vtype(prevview);
            if (side2 != side) {
                sprintf(tmpbuf, "Up to date; had seen %s %s.",
                      (side2 == NULL ? "independent" :
                       side_name(side2)),
                      u_type_name(u));
            } else {
                sprintf(tmpbuf,
                      "Up to date; had been occupied by your %s.",
                      u_type_name(u));
            }
          }
      } else {
          sprintf(tmpbuf, "Up to date.");
      }
    } else {
      if (prevview == EMPTY) {
          sprintf(tmpbuf, "Was empty %d turns ago.", age);
      } else if (prevview == UNSEEN) {
          sprintf(tmpbuf, "Terrain first seen %d turns ago.", age);
      } else {
          side2 = side_n(vside(prevview));
          u = vtype(prevview);
          if (side2 != side) {
            sprintf(tmpbuf, "Saw %s %s, %d turns ago.",
                  (side2 == NULL ? "independent" :
                   side_name(side2)),
                  u_type_name(u), age);
          } else {
            sprintf(tmpbuf, "Was occupied by your %s %d turns ago.",
                  u_type_name(u), age);
          }
      }
    }
#endif

void
size_desc(char *buf, Unit *unit, int label)
{
    buf[0] = '\0';
    if (!u_advanced(unit->type))
      return;
    tprintf(buf, "(%d)", unit->size);
}

void
hp_desc(char *buf, Unit *unit, int label)
{
    if (label) {
      sprintf(buf, "HP ");
    } else {
      buf[0] = '\0';
    }
    /* (print '-' or some such for zero hp case?) */
    if (unit->hp == u_hp(unit->type)) {
      tprintf(buf, "%d", unit->hp);
    } else {
      tprintf(buf, "%d/%d", unit->hp, u_hp(unit->type));
    } 
}

void
acp_desc(char *buf, Unit *unit, int label)
{
    int u = unit->type;

    if (!completed(unit)) {
      sprintf(buf, "%d/%d done", unit->cp, u_cp(u));
    } else if (unit->act && u_acp(u) > 0) {
      if (label) {
          strcpy(buf, "ACP ");
      } else {
          buf[0] = '\0';
      }
      if (unit->act->acp == unit->act->initacp) {
          tprintf(buf, "%d", unit->act->acp);
      } else {
          tprintf(buf, "%d/%d", unit->act->acp, unit->act->initacp);
      }
    } else {
      buf[0] = '\0';
    }
}

/* Describe a unit's current combat experience, if applicable. */

void
cxp_desc(char *buf, Unit *unit, int label)
{
    int cxpmax = u_cxp_max(unit->type);

    buf[0] = '\0';
    if (cxpmax == 0)
      return;
    if (label)
      strcat(buf, "  cXP ");
    if (unit->cxp == cxpmax) {
      tprintf(buf, "%d", unit->cxp);
    } else {
      tprintf(buf, "%d/%d", unit->cxp, cxpmax);
    } 
}

/* Describe a unit's current morale, if applicable. */

void
morale_desc(char *buf, Unit *unit, int label)
{
    int moralemax = u_morale_max(unit->type);

    buf[0] = '\0';
    if (moralemax == 0)
      return;
    if (label)
      strcat(buf, "  Mor ");
    if (unit->morale == moralemax) {
      tprintf(buf, "%d", unit->morale);
    } else {
      tprintf(buf, "%d/%d", unit->morale, moralemax);
    } 
}

/* Describe a unit's individual point value if it has one. */

void
point_value_desc(char *buf, Unit *unit, int label)
{
    buf[0] = '\0';
    if (unit_point_value(unit) < 0)
      return;
    if (label)
      strcat(buf, "  Value ");
    tprintf(buf, "%d", unit_point_value(unit));
}

/* Describe one "row" (group of 3) of a unit's supply status. */

int
supply_desc(char *buf, Unit *unit, int mrow)
{
    int u = unit->type, m, mm, tmprow;

    tmprow = 0;
    buf[0] = '\0';
    mm = 0;
    for_all_material_types(m) {
      if (um_storage_x(u, m) > 0) {
          if (mm > 0 && mm % 3 == 0)
            ++tmprow;
          if (tmprow == mrow) {
            tprintf(buf, "%s %d/%d  ",
                  m_type_name(m), unit->supply[m], um_storage_x(u, m));
          }
          ++mm;
      }
    }
    return (strlen(buf) > 0);
}

/* Describe a builder's current tooling. */

int
tooling_desc(char *buf, Unit *unit)
{
    int u2, num;

    buf[0] = '\0';
    if (unit->tooling == NULL)
      return FALSE;
    /* Use the number of nonzero tooling to govern the use of
       abbreviations. */
    num = 0;
    for_all_unit_types(u2)
      if (unit->tooling[u2] > 0)
      ++num;
    if (num == 0) {
      strcat(buf, "No tooling");
      return TRUE;
    }
    strcat(buf, "Tooling: ");
    for_all_unit_types(u2) {
      if (unit->tooling[u2] > 0) {
          tprintf(buf, "%s %d/%d  ",
                (num > 2 ? shortest_unique_name(u2) : u_type_name(u2)),
                unit->tooling[u2], uu_tp_to_build(unit->type, u2));
      }
    }
    return TRUE;
}

void
location_desc(char *buf, Side *side, Unit *unit, int u, int x, int y)
{
    int t = terrain_at(x, y);
    char *featurename;

    if (unit != NULL && unit->transport != NULL) {
      sprintf(buf, "In %s", short_unit_handle(unit->transport));
    } else if (unit != NULL || u != NONUTYPE) {
      sprintf(buf, "In %s", t_type_name(t));
      linear_desc(buf + strlen(buf), x, y);
    } else if (terrain_visible(side, x, y)) {
      sprintf(buf, "Empty %s", t_type_name(t));
      linear_desc(buf + strlen(buf), x, y);
    } else {
      sprintf(buf, "Unknown");
    }
    if (terrain_visible(side, x, y)) {
      featurename = feature_name_at(x, y);
      if (!empty_string(featurename))
        tprintf(buf, " (%s)", featurename);
      if (elevations_defined())
        tprintf(buf, " (El %d)", elev_at(x, y));
      if (temperatures_defined())
        tprintf(buf, " (T %d)", temperature_at(x, y));
      if (winds_defined()) {
          int wforce = wind_force_at(x, y);

          if (wforce == 0)
            tprintf(buf, " (W calm)");
          else
            tprintf(buf, " (W f%d %s)", wforce, dirnames[wind_dir_at(x, y)]);
      }
      /* (should optionally list other local weather also) */
    }
    tprintf(buf, " at %d,%d", x, y);
}

/* Given a cell, describe where it is. */

void
destination_desc(char *buf, Side *side, Unit *unit, int x, int y, int z)
{
    int dir, x1, y1;
    Unit *unit2;

    if (!in_area(x, y)) {
      sprintf(buf, "?%d,%d?", x, y);
      return;
    }
    unit2 = unit_at(x, y);
    if (unit2 != NULL && unit2->side == side) {
      if (unit2 == unit) {
          sprintf(buf, "self (%d,%d)", x, y);
      } else if (!empty_string(unit2->name)) {
          sprintf(buf, "%s (%d,%d)", unit2->name, x, y);
      } else {
          sprintf(buf, "%s (%d,%d)", u_type_name(unit2->type), x, y);
      }
      return;
    }
    for_all_directions(dir) {
      if (interior_point_in_dir(x, y, dir, &x1, &y1)) {
          unit2 = unit_at(x1, y1);
          /* (should look at stack) */
          if (unit2 != NULL && unit2->side == side) {
            if (unit2 == unit) {
                sprintf(buf, "%s (%d,%d)",
                      dirnames[opposite_dir(dir)], x, y);
            } else if (!empty_string(unit2->name)) {
                sprintf(buf, "%s of %s (%d,%d)",
                      dirnames[opposite_dir(dir)], unit2->name, x, y);
            } else {
                sprintf(buf, "%s of %s (%d,%d)",
                      dirnames[opposite_dir(dir)],
                      u_type_name(unit2->type), x, y);
            }
            return;
          }
      }
    }
    /* This is the old reliable case. */
    sprintf(buf, "%d,%d", x, y);
    if (z != 0)
      tprintf(buf, ",%d", z);
}

void
latlong_desc(char *buf, int x, int y, int xf, int yf, int which)
{
    char minbuf[10];
    int rawlat, latdeg, latmin, rawlon, londeg, lonmin;

    buf[0] = '\0';
    if (world.circumference <= 0)
      return;
    xy_to_latlong(x, y, xf, yf, &rawlat, &rawlon);
    if (which & 2) {
      latdeg = abs(rawlat) / 60;
      latmin = abs(rawlat) % 60;
      minbuf[0] = '\0';
      if (latmin != 0)
        sprintf(minbuf, "%dm", latmin);
      sprintf(buf, "%dd%s %c",
            latdeg, minbuf, (rawlat >= 0 ? 'N' : 'S'));
    }
    if (which & 1) {
      londeg = abs(rawlon) / 60;
      lonmin = abs(rawlon) % 60;
      minbuf[0] = '\0';
      if (lonmin != 0)
        sprintf(minbuf, "%dm", lonmin);
      if (!empty_string(buf))
        strcat(buf, " ");
      sprintf(buf + strlen(buf), "%dd%s %c",
            londeg, minbuf, (rawlon >= 0 ? 'E' : 'W'));
    }
}

void
others_here_desc(char *buf, Unit *unit)
{
    int u2, first = TRUE, nums[MAXUTYPES], incomplete[MAXUTYPES];
    Unit *unit2, *top;

    buf[0] = '\0';
    top = unit_at(unit->x, unit->y);
    if (top != NULL && top->nexthere != NULL) {
      for_all_unit_types(u2)
        nums[u2] = incomplete[u2] = 0;
      for_all_stack(unit->x, unit->y, unit2)
        if (completed(unit2))
          ++nums[unit2->type];
        else
          ++incomplete[unit2->type];
      /* Don't count ourselves. */
      if (completed(unit))
        --nums[unit->type];
      else
        --incomplete[unit->type];
      for_all_unit_types(u2) {
          if (nums[u2] > 0 || incomplete[u2] > 0) {
            if (first)
              first = FALSE;
            else
              strcat(buf, " ");
            if (nums[u2] > 0)
              tprintf(buf, "%d", nums[u2]);
            if (incomplete[u2] > 0)
              tprintf(buf, "(%d)", incomplete[u2]);
            strcat(buf, " ");
            strcat(buf, shortest_generic_name(u2));
          }
      }
      strcat(buf, " here also");
    }
}

void
occupants_desc(char *buf, Unit *unit)
{
    int u2, first = TRUE, nums[MAXUTYPES], incomplete[MAXUTYPES];
    Unit *occ;

    buf[0] = '\0';
    if (unit->occupant != NULL) {
      strcat(buf, "Occs ");
      for_all_unit_types(u2)
        nums[u2] = incomplete[u2] = 0;
      for_all_occupants(unit, occ)
        if (completed(occ))
          ++nums[occ->type];
        else
          ++incomplete[occ->type];
      for_all_unit_types(u2) {
          if (nums[u2] > 0 || incomplete[u2] > 0) {
            if (first)
              first = FALSE;
            else
              strcat(buf, " ");
            if (nums[u2] > 0)
              tprintf(buf, "%d", nums[u2]);
            if (incomplete[u2] > 0)
              tprintf(buf, "(%d)", incomplete[u2]);
            strcat(buf, " ");
            strcat(buf, shortest_generic_name(u2));
          }
      }
    }
}

/* Fill the given buffer with a verbal description of the unit's
   current plan. */

void
plan_desc(char *buf, Unit *unit)
{
    char goalbuf[BUFSIZE];
    int i;
    Plan *plan = unit->plan;
    Task *task;

    if (plan == NULL) {
      buf[0] = '\0';
      return;
    }
    sprintf(buf, "Plan: %s", plantypenames[plan->type]);
    if (plan->waitingfortasks)
      strcat(buf, " Waiting");
    if (plan->asleep)
      strcat(buf, " Asleep");
    if (plan->reserve)
      strcat(buf, " Reserve");
    if (plan->delayed)
      strcat(buf, " Delay");
    /* The more usual case is to allow AI control of units, so we only
       mention it when it's off. */
    if (!plan->aicontrol)
      strcat(buf, " NoAI");
    if (plan->supply_is_low)
      strcat(buf, " SupplyLow");
    if (plan->waitingfortransport)
      strcat(buf, " WaitingForTransport");
    if (plan->maingoal) {
      strcat(buf, ". Goal: ");
      strcat(buf, goal_desc(goalbuf, plan->maingoal));
    }
    if (plan->formation) {
      strcat(buf, ". Formation: ");
      strcat(buf, goal_desc(goalbuf, plan->formation));
    }
    /* For tasks, just put out a count. */
    if (plan->tasks) {
      i = 0;
      for_all_tasks(plan, task)
        ++i;
      tprintf(buf, ". %d task%s", i, (i == 1 ? "" : "s"));
    } 
    strcat(buf, ".");
}

/* Describe a task in a brief but intelligible way.  Note that the
   given side and/or unit may be NULL, such as when describing a task
   not yet assigned to a unit (standing orders for instance). */

void
task_desc(char *buf, Side *side, Unit *unit, Task *task)
{
    int i, slen, arg0, arg1, arg2, arg3;
    char *argtypes, *sidedesc, *utypedesc;
    Unit *unit2;

    if (task == NULL) {
      buf[0] = '\0';
      return;
    }
    arg0 = task->args[0];
    arg1 = task->args[1];
    arg2 = task->args[2];
    arg3 = task->args[3];
    sprintf(buf, "%s ", taskdefns[task->type].display_name);
    switch (task->type) {
      case TASK_BUILD:
      tprintf(buf, "%s", u_type_name(arg0));
      if (arg1 != 0) {
          unit2 = find_unit(arg1);
          if (unit2 != NULL) {
            tprintf(buf, ", %d/%d done", unit2->cp, u_cp(unit2->type));
          }
      }
      if (arg3 > 1)
        tprintf(buf, " (%d%s of %d)",
              arg2 + 1, ordinal_suffix(arg2 + 1), arg3);
      break;
      case TASK_CAPTURE:
      sidedesc = (arg3 == ALLSIDES ? "any" : side_adjective(side_n(arg3)));
      utypedesc = (arg2 == NONUTYPE ? "unit" : u_type_name(arg2));
      tprintf(buf, "%s %s ", sidedesc, utypedesc);
      unit2 = unit_at(arg0, arg1);
      /* (should scan stack, only list a visible unit) */
      if (unit2 != NULL 
          && unit2->side == side_n(arg3)
          && !empty_string(unit2->name)) {
          tprintf(buf, "%s ", unit2->name);
          tprintf(buf, "(%d,%d)", arg0, arg1);
      } else {
          tprintf(buf, "at %d,%d", arg0, arg1);
      }
      break;
      case TASK_COLLECT:
      tprintf(buf, "%s around ", m_type_name(arg0));
      tprintf(buf, "%d,%d", arg0, arg1);
      break;
      case TASK_DEVELOP:
      tprintf(buf, "%s to tech %d", u_type_name(arg0), arg1);
      if (side != NULL) {
          tprintf(buf, " (currently at %d)", side->tech[arg0]);
      }
      break;
      case TASK_HIT_POSITION:
      tprintf(buf, "at %d,%d", arg0, arg1);
      break;
      case TASK_HIT_UNIT:
      sidedesc = (arg3 == ALLSIDES ? "any" : side_adjective(side_n(arg3)));
      utypedesc = (arg2 == NONUTYPE ? "unit" : u_type_name(arg2));
      tprintf(buf, "%s %s ", sidedesc, utypedesc);
      unit2 = unit_at(arg0, arg1);
      if (unit2 != NULL 
          && unit2->side == side_n(arg3)
          && !empty_string(unit2->name)) {
          tprintf(buf, "%s ", unit2->name);
          tprintf(buf, "(%d,%d)", arg0, arg1);
      } else {
          tprintf(buf, "at %d,%d", arg0, arg1);
      }
      break;
      case TASK_MOVE_DIR:
      tprintf(buf, "%s, %d times", dirnames[arg0], arg1);
      break;
      case TASK_MOVE_TO:
      if (unit != NULL && unit->x == arg0 && unit->y == arg1) {
          tprintf(buf, "here");
      } else {
          if (arg3 == 0) {
            /* do nothing */
          } else if (arg3 == 1) {
            tprintf(buf, "adj ");
          } else {
            tprintf(buf, "within %d of ", arg3);
          }
          tprintf(buf, "%d,%d", arg0, arg1);
      }
        break;
      case TASK_OCCUPY:
      unit2 = find_unit(arg0);
      if (unit2 != NULL) {
          tprintf(buf, "%s ", medium_long_unit_handle(unit2));
          if (!empty_string(unit2->name)) {
            tprintf(buf, "(%d,%d)", unit2->x, unit2->y);
          } else {
            tprintf(buf, "at %d,%d", unit2->x, unit2->y);
          }
      } else {
          tprintf(buf, "unknown unit #%d", arg0);
      }
      break;
      case TASK_PICKUP:
      unit2 = find_unit(arg0);
      if (unit2 != NULL) {
          tprintf(buf, "%s", medium_long_unit_handle(unit2));
      } else {
          tprintf(buf, "unknown unit #%d", arg0);
      }
      break;
      case TASK_RESUPPLY:
      if (arg0 != NONMTYPE)
        tprintf(buf, "%s", m_type_name(arg0));
      else
        tprintf(buf, "all");
      if (arg1 != 0) {
          unit2 = find_unit(arg1);
          if (unit2 != NULL) {
            tprintf(buf, " at %s", short_unit_handle(unit2));
          }
      }
      break;
      case TASK_SENTRY:
      /* (should display as calendar time) */
      tprintf(buf, "for %d turns", arg0);
      break;
      default:
      /* Default is just to dump out the raw data about the task. */
      tprintf(buf, "raw");
      argtypes = taskdefns[task->type].argtypes;
      slen = strlen(argtypes);
      for (i = 0; i < slen; ++i) {
          tprintf(buf, "%c%d", (i == 0 ? ' ' : ','), task->args[i]);
      }
      break;
    }
    if (Debug) {
      /* Include the number of executions and retries. */
      tprintf(buf, " x %d", task->execnum);
      if (task->retrynum > 0) {
          tprintf(buf, " fail %d", task->retrynum);
      }
    }
}

/* Describe a goal in a human-intelligible way. */

char *
goal_desc(char *buf, Goal *goal)
{
    int numargs, i, arg;
    char *argtypes;
    Unit *unit;
    
    if (goal == NULL)
      return "null goal";
    switch (goal->type) {
      case GOAL_KEEP_FORMATION:
      sprintf(buf, " %d,%d from %s (var %d)",
            goal->args[1], goal->args[2],
            unit_handle(NULL, find_unit(goal->args[0])),
            goal->args[3]);
      break;
      case GOAL_VICINITY_HELD:
      sprintf(buf, "Hold %dx%d area around %d,%d",
            goal->args[2], goal->args[3], goal->args[0], goal->args[1]);
      break;
      case GOAL_UNIT_OCCUPIED:
      unit = find_unit(goal->args[0]);
      sprintf(buf, "Occupy %s ", medium_long_unit_handle(unit));
          if (!empty_string(unit->name)) {
            tprintf(buf, "(%d,%d)", unit->x, unit->y);
          } else {
            tprintf(buf, "at %d,%d", unit->x, unit->y);
          }
      break;
      /* (should add more cases specific to common types of goals) */
      default:
      sprintf(buf, "%s%s", (goal->tf ? "" : "not "),
            goaldefns[goal->type].display_name);
      argtypes = goaldefns[goal->type].argtypes;
      numargs = strlen(argtypes);
      for (i = 0; i < numargs; ++i) {
          arg = goal->args[i];
          switch (argtypes[i]) {
            case 'h':
            tprintf(buf, "%d", arg);
            break;
            case 'm':
            if (is_material_type(arg))
              tprintf(buf, " %s", m_type_name(arg));
            else
              tprintf(buf, " m%d?", arg);
            break;
            case 'S':
            tprintf(buf, " %s", short_side_title(side_n(arg)));
            break;
            case 'u':
            if (is_unit_type(arg))
              tprintf(buf, " %s", u_type_name(arg));
            else
              tprintf(buf, " u%d?", arg);
            break;
            case 'U':
            tprintf(buf, " %s", unit_handle(NULL, find_unit(arg)));
            break;
            case 'w':
            tprintf(buf, " %dx", arg);
            break;
            case 'x':
            tprintf(buf, " %d,", arg);
            break;
            case 'y':
            tprintf(buf, "%d", arg);
            break;
            default:
            tprintf(buf, " %d", arg);
            break;
          }
      }
      break;
    }
    return buf;
}

/* Format a (real, not game) clock time into a standard form.  This
   routine will omit the hours part if it will be uninteresting. */

void
time_desc(char *buf, int time, int maxtime)
{
    int hour, minute, second;

    if (time >= 0) {
      hour = time / 3600;  minute = (time / 60) % 60;  second = time % 60;
      if (between(1, maxtime, 3600) && hour == 0) {
          sprintf(buf, "%.2d:%.2d", minute, second);
      } else {
          sprintf(buf, "%.2d:%.2d:%.2d", hour, minute, second);
      }
    } else {
      sprintf(buf, "??:??:??");
    }
}

#if 0       /* Unused. */

/* General-purpose routine to take an array of anonymous unit types and
   summarize what's in it, using numbers and unit chars. */

char *
summarize_units(char *buf, int *ucnts)
{
    char tmp[BUFSIZE];  /* should be bigger? */
    int u;

    buf[0] = '\0';
    for_all_unit_types(u) {
      if (ucnts[u] > 0) {
          sprintf(tmp, " %d %s", ucnts[u], utype_name_n(u, 3));
          strcat(buf, tmp);
      }
    }
    return buf;
}

#endif

static void notify_doctrine_1(Side *side, Doctrine *doctrine);

void
notify_doctrine(Side *side, char *spec)
{
    int u;
    char *arg, *rest, substr[BUFSIZE], outbuf[BUFSIZE];
    Doctrine *doctrine;

    if (!empty_string(spec))
      rest = get_next_arg(spec, substr, &arg);
    else
      arg = "";
    if ((doctrine = find_doctrine_by_name(arg)) != NULL) {
      /* Found a specific named doctrine. */
      /* (should say which of our own unit types use it) */
    } else if ((u = utype_from_name(arg)) != NONUTYPE) {
      doctrine = side->udoctrine[u];
    } else if (strcmp(arg, "default") == 0) {
      doctrine = side->default_doctrine;
      /* (should say which unit types use it) */
    } else {
      if (!empty_string(arg))
        notify(side, "\"%s\" not recognized as doctrine name or unit type.",
             arg);
      /* Note that although we list all doctrines, including those
         belonging to other sides, because we don't know if the
         other side is actually *using* any particular doctrine.
         While this may seem like an information leak, if a side has
         its own unique doctrine, curiously enough this corresponds
         well to the real world; for instance, present-day nations'
         militaries are all familiar with each other's doctrines. */
      outbuf[0] = '\0';
      for_all_doctrines(doctrine) {
          if (!empty_string(doctrine->name))
            tprintf(outbuf, " %s", doctrine->name);
          else
            tprintf(outbuf, " #%d", doctrine->id);
          if (doctrine->next != NULL)
            tprintf(outbuf, ",");
      }
      notify(side, "Doctrines available:%s", outbuf);
      for_all_doctrines(doctrine) {
          notify_doctrine_1(side, doctrine);
      }
      return;
    }
    /* We now have a doctrine to display. */
    notify_doctrine_1(side, doctrine);
}

static void
notify_doctrine_1(Side *side, Doctrine *doctrine)
{
    int u, i, num, dflt;
    char abuf[BUFSIZE];

    if (!empty_string(doctrine->name))
      notify(side, "Doctrine '%s' (%s):",
           doctrine->name, (doctrine->locked ? "fixed" : "adjustable"));
    else
      notify(side, "Doctrine #%d (%s):",
           doctrine->id, (doctrine->locked ? "fixed" : "adjustable"));
    notify(side, "  Resupply at %d%% of storage", doctrine->resupply_percent);
    notify(side, "  Rearm at %d%% of storage", doctrine->rearm_percent);
    notify(side, "  Repair at %d%% of hp", doctrine->repair_percent);
    abuf[0] = '\0';
    i = 0;
    num = 0;
    dflt = doctrine->construction_run[0];
#if 0 /* should use histo code in write_table */
    for_all_unit_types(u) {
      if (dflt == doctrine->construction_run[u]) {
          ++num;
      }
    }
#endif
    for_all_unit_types(u) {
      tprintf(abuf, "  %s %d",
            u_type_name(u), doctrine->construction_run[u]);
      if (i > 0 && (i++) % 4 == 0 && !empty_string(abuf)) {
          notify(side, "  Construction run:%s", abuf);
          abuf[0] = '\0';
      }
    }
    if (!empty_string(abuf))
      notify(side, "  Construction run:%s", abuf);
}

#define first_person(unit) ((unit)->side && (unit)->side->self_unit == (unit))

static int report_combat_special(Unit *unit1, Unit *unit2, char *str);
int type_matches_symbol(Obj *sym, int u);

void
report_combat(Unit *atker, Unit *other, char *str)
{
    int rslt;

    if (g_action_notices() != lispnil) {
      rslt = report_combat_special(atker, other, str);
      if (rslt)
        return;
    }
    /* Default messages for each type of report. */
    if (strcmp(str, "destroy") == 0) {
      notify_combat(atker, other, (first_person(atker) ? "%s destroy %s!" : "%s destroys %s!"));
    } else if (strcmp(str, "destroy-occupant") == 0) {
      notify_combat(other, atker, (first_person(atker) ? "  (and destroy occupant %s!)" : "  (and destroys occupant %s!)"));
    } else if (strcmp(str, "hit") == 0) {
      notify_combat(atker, other, (first_person(atker) ? "%s hit %s!" : "%s hits %s!"));
    } else if (strcmp(str, "hit-occupant") == 0) {
      notify_combat(other, atker, (first_person(atker) ? "  (and hit occupant %s!)" : "  (and hits occupant %s!)"));
    } else if (strcmp(str, "miss") == 0) {
      notify_combat(atker, other, (first_person(atker) ? "%s miss %s." : "%s misses %s."));
    } else if (strcmp(str, "miss-occupant") == 0) {
      /* (this case is too uninteresting to mention) */
    } else if (strcmp(str, "resist") == 0) {
      notify_combat(other, atker, (first_person(other) ? "%s throw back %s!" : "%s throws back %s!"));
    } else if (strcmp(str, "resist/slaughter") == 0) {
      notify_combat(other, atker, (first_person(other) ? "%s resist capture; %s slaughtered!" : "%s resists capture; %s slaughtered!"));
    } else if (strcmp(str, "capture") == 0) {
      notify_combat(atker, other, (first_person(atker) ? "%s capture %s!" : "%s captures %s!"));
    } else if (strcmp(str, "liberate") == 0) {
      notify_combat(atker, other, (first_person(atker) ? "%s liberate %s!" : "%s liberates %s!"));
    } else if (strcmp(str, "escape") == 0) {
      notify_combat(other, atker, (first_person(other) ? "%s escape!" : "%s escapes!"));
    } else if (strcmp(str, "retreat") == 0) {
      notify_combat(other, atker, (first_person(other) ? "%s retreat!" : "%s retreats!"));
    } else if (strcmp(str, "detonate") == 0) {
      notify_combat(atker, other, (first_person(atker) ? "%s detonate!" : "%s detonates!"));
    } else {
      notify_combat(atker, other, str);
    }
}

static void
notify_combat(Unit *unit1, Unit *unit2, char *str)
{
    char buf1[BUFSIZE], buf2[BUFSIZE];
    Side *side3;

    for_all_sides(side3) {
      if (active_display(side3)
          && (side3->see_all
            || (unit1 != NULL && side3 == unit1->side)
            || (unit2 != NULL && side3 == unit2->side))) {
          strcpy(buf1, unit_handle(side3, unit1));
          strcpy(buf2, unit_handle(side3, unit2));
          notify(side3, str, buf1, buf2);
      }
    }
}

static int
report_combat_special(Unit *unit1, Unit *unit2, char *str)
{
    int found = FALSE;
    char abuf[BUFSIZE];
    Obj *rest, *head, *pat, *msgdesc;
    Side *side3;

    for_all_list(g_action_notices(), rest) {
      head = car(rest);
      if (!consp(head)) {
          run_warning("Non-list in action-notices");
          continue;
      }
      pat = car(head);
      if (symbolp(pat) && strcmp(c_string(pat), str) == 0) {
          found = TRUE;
          break;
      }
      if (consp(pat)
          && symbolp(car(pat))
          && strcmp(c_string(car(pat)), str) == 0
          && pattern_matches_combat(cdr(pat), unit1, unit2)) {
          found = TRUE;
          break;
      }
    }
    /* If we have a match, do something with it. */
    if (found) {
      msgdesc = cadr(head);
      if (stringp(msgdesc)) {
          strcpy(abuf, c_string(msgdesc));
      } else {
          /* Notify all sides that could have seen the combat. */
          for_all_sides(side3) {
            if (active_display(side3)
                && (side3->see_all
                  || (unit1 != NULL && side3 == unit1->side)
                  || (unit2 != NULL && side3 == unit2->side))) {
                combat_desc_from_list(side3, msgdesc, unit1, unit2, str,
                                abuf);
                notify(side3, abuf);
            }
          }
      }
    }
    return found;
}

static int
pattern_matches_combat(Obj *parms, Unit *unit, Unit *unit2)
{
    Obj *head;

    head = car(parms);
    if (!type_matches_symbol(head, unit->type))
      return FALSE;
    parms = cdr(parms);
    head = car(parms);
    if (!type_matches_symbol(head, unit2->type))
      return FALSE;
    return TRUE;
}

int
type_matches_symbol(Obj *sym, int u)
{
    char *typename;
    Obj *val, *rest, *head;

    if (!symbolp(sym))
      return FALSE;
    typename = u_type_name(u);
    if (strcmp(c_string(sym), typename) == 0)
      return TRUE;
    if (match_keyword(sym, K_USTAR))
      return TRUE;
    if (boundp(sym)) {
      val = eval(sym);
      if (symbolp(val) && strcmp(c_string(val), typename) == 0)
        return TRUE;
      if (utypep(val) && c_number(val) == u)
        return TRUE;
      if (consp(val)) {
          for_all_list(val, rest) {
            head = car(rest);
            if (symbolp(head) && strcmp(c_string(head), typename) == 0)
              return TRUE;
            if (utypep(head) && c_number(head) == u)
              return TRUE;
          }
      }
    }
    return FALSE;
}

static void
combat_desc_from_list(Side *side, Obj *lis, Unit *unit, Unit *unit2,
                  char *str, char *buf)
{
    int n;
    char *symname;
    Obj *rest, *item;

    buf[0] = '\0';
    for_all_list(lis, rest) {
      item = car(rest);
      if (stringp(item)) {
          strcat(buf, c_string(item));
      } else if (symbolp(item)) {
          symname = c_string(item);
          if (strcmp(symname, "actor") == 0) {
            strcat(buf, unit_handle(side, unit));
          } else if (strcmp(symname, "actee") == 0) {
            strcat(buf, unit_handle(side, unit2));
          } else {
            tprintf(buf, " ??%s?? ", symname);
          }
      } else if (numberp(item)) {
          n = c_number(item);
          if (0 /* special processing */) {
          } else {
            tprintf(buf, "%d", n);
          }
      } else {
          strcat(buf, " ????? ");
      }
    }
}

void
report_give(Side *side, Unit *unit, Unit *unit2, short *rslts)
{
    char buf[BUFSIZE];
    int m, something = FALSE;

    sprintf(buf, "%s gave", unit_handle(side, unit));
    for_all_material_types(m) {
      if (rslts[m] > 0) {
          tprintf(buf, " %d %s", rslts[m], m_type_name(m));
          something = TRUE;
      }
    }
    if (!something) {
      strcat(buf, " nothing");
    }
    if (unit2 != NULL) {
      tprintf(buf, " to %s", unit_handle(side, unit2));
    }
    notify(side, "%s.", buf);
}

void
report_take(Side *side, Unit *unit, int needed, short *rslts)
{
    char buf[BUFSIZE];
    int m, something = FALSE;

    if (!needed) {
      notify(side, "%s needed nothing.", unit_handle(side, unit));
      return;
    }
    buf[0] = '\0';
    for_all_material_types(m) {
      if (rslts[m] > 0) {
          tprintf(buf, " %d %s", rslts[m], m_type_name(m));
          something = TRUE;
      }
    }
    if (something) {
      notify(side, "%s got%s.", unit_handle(side, unit), buf);
    } else {
      notify(side, "%s got nothing.", unit_handle(side, unit));
    }
}

void
notify_all_of_resignation(Side *side, Side *side2)
{
    Side *side3;

    for_all_sides(side3) {
      if (side3 != side) {
          notify(side3,
               "%s %s giving up!",
               short_side_title_with_adjective(side,
#ifdef RUDE
               (flip_coin() ? "cowardly" : "wimpy")
#else
               NULL
#endif /* RUDE */
               ),
               (short_side_title_plural_p(side) ? "are" : "is"));
          if (side2 != NULL) {
            notify(side3, "... and donating everything to %s!",
                   short_side_title(side2));
          }
      }
    }
}

/* Given a number, figure out what suffix should go with it. */

char *
ordinal_suffix(int n)
{
    if (n % 100 == 11 || n % 100 == 12 || n % 100 == 13) {
      return "th";
    } else {
      switch (n % 10) {
        case 1:   return "st";
        case 2:   return "nd";
        case 3:   return "rd";
        default:  return "th";
      }
    }
}

/* Pluralize a word, attempting to be smart about various
   possibilities that don't have a different plural form (such as
   "Chinese" and "Swiss"). */

/* There should probably be a test for when to add "es" instead of "s". */

char *
plural_form(char *word)
{
    char endch = ' ', nextend = ' ';
    int len;

    if (word == NULL) {
      run_warning("plural_form given NULL string");
      pluralbuf[0] = '\0';
      return pluralbuf;
    }
    len = strlen(word);
    if (len > 0)
      endch = word[len - 1];
    if (len > 1)
      nextend = word[len - 2];
    if (endch == 'h' || endch == 's' || (endch == 'e' && nextend == 's')) {
      sprintf(pluralbuf, "%s", word);
    } else {
      sprintf(pluralbuf, "%ss", word);
    }
    return pluralbuf;
}

/* Make the first letter of the buffer upper-case. */

char *
capitalize(char *buf)
{
    if (islower(buf[0]))
      buf[0] = toupper(buf[0]);
    return buf;
}

/* Compose a readable form of the given date. */

char *
absolute_date_string(int date)
{
    /* The first time we ask for a date, interpret the calendar. */
    if (calendar_type == cal_unknown)
      init_calendar();
    switch (calendar_type) {
      case cal_number:
      sprintf(datebuf, "%s%4d", turn_name, date);
      return datebuf;
      case cal_usual:
      return usual_date_string(date);
      default:
      case_panic("calendar type", calendar_type);
    }
    return "!?!";
}

/* Interpret the calendar definition. */

static void
init_calendar(void)
{
    Obj *cal, *caltype, *stepname, *steprest;

    cal = g_calendar();
    turn_name = "Turn";
    if (cal == lispnil) {
      /* The default is to have numbered turns named "Turn". */
      calendar_type = cal_number;
    } else if (consp(cal)) {
      caltype = car(cal);
      if (match_keyword(caltype, K_NUMBER)) {
          calendar_type = cal_number;
          if (stringp(cadr(cal)))
            turn_name = c_string(cadr(cal));
      } else if (match_keyword(caltype, K_USUAL)) {
          calendar_type = cal_usual;
          stepname = cadr(cal);
          if (symbolp(stepname)) {
            parse_date_step_range(cdr(cal));
          } else if (consp(stepname)) {
            for_all_list(cdr(cal), steprest) {
                parse_date_step_range(car(steprest));
            }
          } else {
            init_warning("No name for date step type, substituting `day'");
            date_step_ranges[0].step_type = ds_day;
            date_step_ranges[0].step_size = 1;
            num_date_step_ranges = 1;
          }
      }
    }
    if (calendar_type == cal_unknown)
      init_error("Bad calendar type");
    if (!empty_string(g_initial_date()))
      set_initial_date(g_initial_date());
}

static void
parse_date_step_range(Obj *form)
{
    int n;
    char *stepnamestr;
    UsualDateStepType steptype;
    Obj *step;

    n = num_date_step_ranges;
    /* Peel off a turn range if supplied. */
    if (numberp(car(form))) {
      date_step_ranges[n].turn_start = c_number(car(form));
      form = cdr(form);
    }
    if (numberp(car(form))) {
      date_step_ranges[n].turn_end = c_number(car(form));
      form = cdr(form);
    }
    if (symbolp(car(form))) {
      stepnamestr = c_string(car(form));
      if (strcmp(stepnamestr, "second") == 0) {
          steptype = ds_second;
      } else if (strcmp(stepnamestr, "minute") == 0) {
          steptype = ds_minute;
      } else if (strcmp(stepnamestr, "hour") == 0) {
          steptype = ds_hour;
      } else if (strcmp(stepnamestr, "day") == 0) {
          steptype = ds_day;
      } else if (strcmp(stepnamestr, "week") == 0) {
          steptype = ds_week;
      } else if (strcmp(stepnamestr, "month") == 0) {
          steptype = ds_month;
      } else if (strcmp(stepnamestr, "season") == 0) {
          steptype = ds_season;
      } else if (strcmp(stepnamestr, "year") == 0) {
          steptype = ds_year;
      } else {
          init_warning("\"%s\" not a known date step name", stepnamestr);
          steptype = ds_day;
      }
    } else {
      /* (should warn) */
      steptype = ds_day;
    }
    date_step_ranges[n].step_type = steptype;
    /* Collect an optional multiple. */
    step = cadr(form);
    date_step_ranges[n].step_size = (numberp(step) ? c_number(step) : 1);
    ++num_date_step_ranges;
}

/* Given two dates, figure out how many turns encompassed by them. */

int
turns_between(char *datestr1, char *datestr2)
{
    int rslt, turn1, turn2;
    UsualDate date1, date2;

    if (calendar_type == cal_unknown)
      init_calendar();
    switch (calendar_type) {
      case cal_number:
      sscanf("%d", datestr1, &turn1);
      sscanf("%d", datestr2, &turn2);
      return (turn2 - turn1);
      case cal_usual:
      parse_usual_date(datestr1, 0, &date1);
      parse_usual_date(datestr2, 0, &date2);
      rslt = date2.year - date1.year;
      if (num_date_step_ranges == 1) {
          if (date_step_ranges[0].step_type == ds_year)
            return (rslt / date_step_ranges[0].step_size);
          if (date_step_ranges[0].step_type < ds_year) {
            rslt = (12 * rslt) - (date2.month - date1.month);
          }
          if (date_step_ranges[0].step_type == ds_month)
            return (rslt / date_step_ranges[0].step_size);
          if (date_step_ranges[0].step_type < ds_month) {
            rslt = (30 * rslt) - (date2.day - date1.day);
          }
          if (date_step_ranges[0].step_type == ds_week)
            return (((rslt + 6) / 7) / date_step_ranges[0].step_size);
          if (date_step_ranges[0].step_type == ds_day)
            return (rslt / date_step_ranges[0].step_size);
          if (date_step_ranges[0].step_type < ds_day) {
            rslt = (24 * rslt) - (date2.hour - date1.hour);
          }
          if (date_step_ranges[0].step_type == ds_hour)
            return (rslt / date_step_ranges[0].step_size);
          return (rslt / date_step_ranges[0].step_size); /* semi-bogus */
      } else {
          /* Too complicated for now, give up. */
          return 1;
      }
      default:
      case_panic("calendar type", calendar_type);
      break;
    }
    return 1;  /* appease the compiler */
}

/* Given a date string, make it be the date of the first turn. */

void
set_initial_date(char *str)
{
    if (calendar_type == cal_unknown)
      init_calendar();
    switch (calendar_type) {
      case cal_number:
      sscanf("%d", str, &turn_initial);
      break;
      case cal_usual:
      if (usual_initial == NULL)
        usual_initial = (UsualDate *) xmalloc(sizeof(UsualDate));
      parse_usual_date(str, 0, usual_initial);
      break;
      default:
      case_panic("calendar type", calendar_type);
      break;
    }
}

/* Pick a date out of the given string. Note that this implementation
   does not detect extra garbage in the string, should fix someday. */

/* (should use strtol etc instead of sscanf) */

static void
parse_usual_date(char *datestr, int range, UsualDate *udate)
{
    char aname[BUFSIZE];
    int i, cnt;
    
    udate->second = udate->minute = udate->hour = 0;
    udate->day = udate->month = udate->year = 0;
    aname[0] = '\0';
    if (!empty_string(datestr)) {
      /* Assume it's in a standard date format. */
      switch (date_step_ranges[0].step_type) {
        case ds_second:
          cnt = sscanf(datestr, "%d:%d:%d %d %s %d",
                   &(udate->second), &(udate->minute), &(udate->hour),
                   &(udate->day), aname, &(udate->year));
          if (cnt != 6) {
            cnt = sscanf(datestr, "%d:%d:%d",
                       &(udate->second), &(udate->minute), &(udate->hour));
            if (cnt != 3)
              goto bad_format;
            return;
          }
          --(udate->day);
          break;
        case ds_minute:
          cnt = sscanf(datestr, "%d:%d %d %s %d",
                   &(udate->minute), &(udate->hour),
                   &(udate->day), aname, &(udate->year));
          if (cnt != 5) {
            cnt = sscanf(datestr, "%d:%d",
                       &(udate->minute), &(udate->hour));
            if (cnt != 2)
              goto bad_format;
            return;
          }
          --(udate->day);
          break;
        case ds_hour:
          cnt = sscanf(datestr, "%d:00 %d %s %d",
                   &(udate->hour),
                   &(udate->day), aname, &(udate->year));
          if (cnt != 4)
            cnt = sscanf(datestr, "%d %d %s %d",
                     &(udate->hour),
                     &(udate->day), aname, &(udate->year));
          if (cnt != 4)
            goto bad_format;
          --(udate->day);
          break;
        case ds_day:
        case ds_week:
          cnt = sscanf(datestr, "%d %s %d",
                   &(udate->day), aname, &(udate->year));
          if (cnt != 3)
            goto bad_format;
          --(udate->day);
          break;
        case ds_month:
          cnt = sscanf(datestr, "%s %d", aname, &(udate->year));
          if (cnt != 2)
            goto bad_format;
          break;
        case ds_season:
          cnt = sscanf(datestr, "%s %d", aname, &(udate->year));
          if (cnt != 2)
            goto bad_format;
          for (i = 0; i < 4; ++i) {
            if (strcmp(aname, seasons[i]) == 0) {
                udate->month = i * 3;
                return;
            }
          }
          init_warning("\"%s\" not a recognized season name", aname);
          return;
        case ds_year:
          cnt = sscanf(datestr, "%d", &(udate->year));
          if (cnt != 1)
            goto bad_format;
          return;
        default:
          init_warning("%d not an allowed date step type",
                   date_step_ranges[0].step_type);
          break;
      }
      for (i = 0; i < 12; ++i) {
          /* (should make case-insensitive) */
          if (strcmp(aname, months[i]) == 0) {
            udate->month = i;
            return;
          }
      }
      init_warning("\"%s\" not a recognized month name", aname);
    }
    return;
  bad_format:
    init_warning("\"%s\" is a badly formatted date", datestr);
}

/* Given a numeric date, convert it into something understandable. */

static int ever_mentioned_bc;

static char *
usual_date_string(int date)
{
    int year = 0, season = 0, month = 0, day = 0;
    int hour = 0, second = 0, minute = 0;
    int i, r, range;

    /* The date, which is a turn number, should be 1 or more, but this
       routine may be called before the game really starts, so return
       something that will be distinctive if it's ever displayed. */
    if (date <= 0)
      return "pregame";
    /* First displayed date is normally turn 1; be zero-based for
       the benefit of calculation. */
    --date;
    /* Find the date step type and size. */
    for (range = 0; range < num_date_step_ranges; ++range) {
      if (between(date_step_ranges[range].turn_start,
                date,
                date_step_ranges[range].turn_end))
        break;
    }
    /* If no matches, just use the last range in the array. */
    if (range >= num_date_step_ranges)
      range = num_date_step_ranges - 1;
    /* If multiples of basic step, convert to basic step by multiplying. */
    date *= date_step_ranges[range].step_size;
    if (usual_initial == NULL) {
      usual_initial = (UsualDate *) xmalloc(sizeof(UsualDate));
      if (!empty_string(g_initial_date()))
        parse_usual_date(g_initial_date(), 0, usual_initial);
    }
    switch (date_step_ranges[range].step_type) {
      case ds_second:
      second = date % 60;
      minute = date / 60;
      sprintf(datebuf, "%d:%d", minute, second);
      /* (should add day/month/year if available?) */
      break;
      case ds_minute:
      minute = date % 60;
      hour = date / 60 + usual_initial->hour;
      sprintf(datebuf, "%d:%d", hour, minute);
      /* (should add day/month/year if available?) */
      break;
      case ds_hour:
      date += usual_initial->hour;
      hour = date % 24;
      date /= 24;
      date += usual_initial->day;
      for (i = 0; i < usual_initial->month; ++i)
        date += monthdays[i];
      day = date % 365;
      month = 0;
      for (i = 0; i < 12; ++i) {
          if (day < monthdays[i])
            break;
          day -= monthdays[i];
          ++month;
      }
      ++day;
      year = date / 365 + usual_initial->year;
      sprintf(datebuf, "%d:00 %2d %s %d",
            hour, day, months[month], ABS(year));
      break;
      case ds_week:
      /* Convert to days, then proceed as for days. */
      date *= 7;
      /* Fall through. */
      case ds_day:
      date += usual_initial->day;
      for (i = 0; i < usual_initial->month; ++i)
        date += monthdays[i];
      day = date % 365;
      month = 0;
      for (i = 0; i < 12; ++i) {
          if (day < monthdays[i])
            break;
          day -= monthdays[i];
          ++month;
      }
      ++day;
      year = date / 365 + usual_initial->year;
      sprintf(datebuf, "%2d %s %d", day, months[month], ABS(year));
      break;
      case ds_month:
      date += usual_initial->month;
      month = date % 12;
      year = date / 12 + usual_initial->year;
      sprintf(datebuf, "%s %d", months[month], ABS(year));
      break;
      case ds_season:
      season = date % 4;
      year = date / 4 + usual_initial->year;
      sprintf(datebuf, "%s %d", seasons[season], ABS(year));
      break;
      case ds_year:
      year = usual_initial->year;
      /* (should do something similar for other step types) */
      for (r = 0; r < range; ++r)
        year += ((date_step_ranges[r].turn_end
                - date_step_ranges[r].turn_start)
               * date_step_ranges[r].step_size);
      year += (date - (date_step_ranges[range].turn_start
                   * date_step_ranges[range].step_size));
      sprintf(datebuf, "%d", ABS(year));
      break;
      default:
      sprintf(datebuf, "%d is unknown date step type",
            date_step_ranges[0].step_type);
      break;
    }
    if (year < 0) {
      strcat(datebuf, " BC");
      ever_mentioned_bc = TRUE;
    } else if (ever_mentioned_bc) {
      /* If any date was displayed as "BC", use "AD" with all positive
         year numbers. */
      strcat(datebuf, " AD");
    }
    return datebuf;
}

/* Show some overall numbers on performance of a side. */

void
write_side_results(FILE *fp, Side *side)
{
    int i;

    if (side == NULL) {
      fprintf(fp, "Results for game as a whole:\n\n");
    } else {
      fprintf(fp, "Results for %s%s",
            short_side_title(side),
            (side_won(side) ? " (WINNER)" :
             (side_lost(side) ? " (LOSER)" :
              "")));
      for (i = 0; i < numscores; ++i) {
          fprintf(fp, " %d", side->scores[i]);
      }
      fprintf(fp, ", played by %s:\n\n",
            long_player_title(spbuf, side->player, NULL));
    }
}

/* Display what is essentially a double-column bookkeeping of unit gains
   and losses. */

void
write_unit_record(FILE *fp, Side *side)
{
    int u, gainreason, lossreason, totgain, totloss, val;

    fprintf(fp, "Unit Record (gains and losses by cause and unit type)\n");
    fprintf(fp, " Unit Type ");
    for (gainreason = 0; gainreason < num_gain_reasons; ++gainreason) {
      fprintf(fp, " %3s", gain_reason_names[gainreason]);
    }
    fprintf(fp, " Gain |");
    for (lossreason = 0; lossreason < num_loss_reasons; ++lossreason) {
      fprintf(fp, " %3s", loss_reason_names[lossreason]);
    }
    fprintf(fp, " Loss |");
    fprintf(fp, " Total\n");
    for_all_unit_types(u) {
      if (u_possible[u]) {
          totgain = 0;
          fprintf(fp, " %9s ", utype_name_n(u, 9));
          for (gainreason = 0; gainreason < num_gain_reasons; ++gainreason) {
            val = gain_count(side, u, gainreason);
            if (val > 0) {
                fprintf(fp, " %3d", val);
                totgain += val;
            } else {
                fprintf(fp, "    ");
            }
          }
          fprintf(fp, "  %3d |", totgain);
          totloss = 0;
          for (lossreason = 0; lossreason < num_loss_reasons; ++lossreason) {
            val = loss_count(side, u, lossreason);
            if (val > 0) {
                fprintf(fp, " %3d", val);
                totloss += val;
            } else {
                fprintf(fp, "    ");
            }
          }
          fprintf(fp, "  %3d |", totloss);
          fprintf(fp, "  %3d\n", totgain - totloss);
      }
    }
    fprintf(fp, "\n");
}

static int
gain_count(Side *side, int u, int r)
{
    int sum;

    if (side != NULL)
      return side_gain_count(side, u, r);
    sum = 0;
    for_all_sides(side) {
      sum += side_gain_count(side, u, r);
    }
    return sum;
}

static int
loss_count(Side *side, int u, int r)
{
    int sum;

    if (side != NULL)
      return side_loss_count(side, u, r);
    sum = 0;
    for_all_sides(side) {
      sum += side_loss_count(side, u, r);
    }
    return sum;
}

/* Nearly-raw combat statistics; hard to interpret, but they provide
   a useful check against subjective evaluation of performance. */

void
write_combat_results(FILE *fp, Side *side)
{
    int a, d, atk;

    fprintf(fp,
          "Unit Combat Results (average damage over # attacks against enemy, by type)\n");
    fprintf(fp, " A  D->");
    for_all_unit_types(d) {
      if (u_possible[d]) {
          fprintf(fp, " %4s ", utype_name_n(d, 4));
      }
    }
    fprintf(fp, "\n");
    for_all_unit_types(a) {
      if (u_possible[a]) {
          fprintf(fp, " %4s ", utype_name_n(a, 4));
          for_all_unit_types(d) {
            if (u_possible[d]) {
                atk = atkstats(side, a, d);
                if (atk > 0) {
                  fprintf(fp, " %5.2f",
                        ((float) hitstats(side, a, d)) / atk);
                } else {
                  fprintf(fp, "      ");
                }
            }
          }
          fprintf(fp, "\n     ");
          for_all_unit_types(d) {
            if (u_possible[d]) {
                atk = atkstats(side, a, d);
                if (atk > 0) {
                  fprintf(fp, " %4d ", atk);
                } else {
                  fprintf(fp, "      ");
                }
            }
          }
          fprintf(fp, "\n");
      }
    }
    fprintf(fp, "\n");
}

static int
atkstats(Side *side, int a, int d)
{
    int sum;

    if (side != NULL)
      return side_atkstats(side, a, d);
    sum = 0;
    for_all_sides(side) {
      sum += side_atkstats(side, a, d);
    }
    return sum;
}

static int
hitstats(Side *side, int a, int d)
{
    int sum;

    if (side != NULL)
      return side_hitstats(side, a, d);
    sum = 0;
    for_all_sides(side) {
      sum += side_hitstats(side, a, d);
    }
    return sum;
}

void
dice_desc(char *buf, int dice)
{
    int numdice, die, offset;

    if (dice >> 14 == 0 || dice >> 14 == 3) {
      sprintf(buf, "%d", dice);
    } else {
      numdice = (dice >> 11) & 0x07;
      die = (dice >> 7) & 0x0f;
      offset = dice & 0x7f;
      if (offset == 0) {
          sprintf(buf, "%dd%d", numdice, die);
      } else {
          sprintf(buf, "%dd%d+%d", numdice, die, offset);
      }
    }
}

/* The following code formats a list of types that are missing images. */

void
record_missing_image(int typtyp, char *str)
{
    if (missinglist == NULL) {
      missinglist = xmalloc(BUFSIZE);
      missinglist[0] = '\0';
    }
    ++missing[typtyp];
    /* Add the name of the image-less type, but only if one of
       the first few. */
    if (between(1, totlisted, NUMTOLIST))
      strcat(missinglist, ",");
    if (totlisted < NUMTOLIST) {
      strcat(missinglist, str);
    } else if (totlisted == NUMTOLIST) {
      strcat(missinglist, "...");
    }
    ++totlisted;
}

/* Return true if any images could not be found, and provide some
   helpful info into the supplied buffer. */

int
missing_images(char *buf)
{
    if (missinglist == NULL)
      return FALSE;
    buf[0] = '\0';
    if (missing[UTYP] > 0)
      tprintf(buf, " %d unit images", missing[UTYP]);
    if (missing[TTYP] > 0)
      tprintf(buf, " %d terrain images", missing[TTYP]);
    if (missing[3] > 0)
      tprintf(buf, " %d emblems", missing[3]);
    tprintf(buf, " - %s", missinglist);
    return TRUE;
}

Generated by  Doxygen 1.6.0   Back to index