/* Online help support 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 is basically support code for interfaces, which handle the
   actual help interaction themselves. */

/* This file must also be translated (mostly) for non-English Xconq. */

#include "conq.h"

int may_detonate(int u);
int any_ut_capacity_x(int u);
int any_mp_to_enter_unit(int u);
int any_mp_to_leave_unit(int u);
int any_enter_indep(int u);

/* Obstack allocation and deallocation routines.  */

#define obstack_chunk_alloc xmalloc
#define obstack_chunk_free free

static void describe_help_system(int arg, char *key, TextBuffer *buf);
static void describe_instructions(int arg, char *key, TextBuffer *buf);
static void describe_synth_run(TextBuffer *buf, int methkey);
static void describe_world(int arg, char *key, TextBuffer *buf);
static int histogram_compare(const void *h1, const void *h2);
static void describe_news(int arg, char *key, TextBuffer *buf);
static void describe_concepts(int arg, char *key, TextBuffer *buf);
static void describe_game_design(int arg, char *key, TextBuffer *buf);
static void describe_utype(int u, char *key, TextBuffer *buf);
static void describe_mtype(int m, char *key, TextBuffer *buf);
static void describe_ttype(int t, char *key, TextBuffer *buf);
static void describe_atype(int t, char *key, TextBuffer *buf);

static void describe_scorekeepers(int arg, char *key, TextBuffer *buf);
static void describe_setup(int arg, char *key, TextBuffer *buf);
static void describe_game_modules(int arg, char *key, TextBuffer *buf);
static void describe_game_module_aux(TextBuffer *buf, Module *module,
				     int level);
static void describe_module_notes(TextBuffer *buf, Module *module);

static int u_property_not_default(int (*fn)(int i), int dflt);
static int t_property_not_default(int (*fn)(int i), int dflt);
static int uu_table_row_not_default(int u, int (*fn)(int i, int j), int dflt);
static int ut_table_row_not_default(int u, int (*fn)(int i, int j), int dflt);
static int um_table_row_not_default(int u, int (*fn)(int i, int j), int dflt);
static int tt_table_row_not_default(int t, int (*fn)(int i, int j), int dflt);
#if 0
static int tm_table_row_not_default(int t, int (*fn)(int i, int j), int dflt);
#endif
static int aa_table_row_not_default(int a1, int (*fn)(int i, int j), int dflt);
static int uu_table_column_not_default(int u, int (*fn)(int i, int j),
				       int dflt);
static int aa_table_column_not_default(int a1, int (*fn)(int i, int j),
				       int dflt);
static void u_property_desc(TextBuffer *buf, int (*fn)(int),
			    void (*formatter)(TextBuffer *, int));
static void t_property_desc(TextBuffer *buf, int (*fn)(int),
			    void (*formatter)(TextBuffer *, int));
static void uu_table_row_desc(TextBuffer *buf, int u, int (*fn)(int, int),
			      void (*formatter)(TextBuffer *, int),
			      char *connect);
static void uu_table_column_desc(TextBuffer *buf, int u, int (*fn)(int, int),
				 void (*formatter)(TextBuffer *, int),
				 char *connect);
static void uu_table_rowcol_desc(TextBuffer *buf, int u, int (*fn)(int, int),
				 void (*formatter)(TextBuffer *, int),
				 char *connect, int rowcol);
static void ut_table_row_desc(TextBuffer *buf, int u, int (*fn)(int, int),
			      void (*formatter)(TextBuffer *, int),
			      char *connect);
static void um_table_row_desc(TextBuffer *buf, int u, int (*fn)(int, int),
			      void (*formatter)(TextBuffer *, int));
static void tt_table_row_desc(TextBuffer *buf, int t1, int (*fn)(int, int),
			      void (*formatter)(TextBuffer *, int));
#if 0
static void tm_table_row_desc(TextBuffer *buf, int t, int (*fn)(int, int),
			      void (*formatter)(TextBuffer *, int));
#endif
static void aa_table_row_desc(TextBuffer *buf, int a1, int (*fn)(int, int),
			      void (*formatter)(TextBuffer *, int));
static void aa_table_column_desc(TextBuffer *buf, int a1, int (*fn)(int, int),
				 void (*formatter)(TextBuffer *, int));
static void aa_table_rowcol_desc(TextBuffer *buf, int a1, int (*fn)(int, int),
				 void (*formatter)(TextBuffer *, int),
				 int rowcol);
static void tb_value_desc(TextBuffer *buf, int val);
static void tb_fraction_desc(TextBuffer *buf, int val);
static void tb_percent_desc(TextBuffer *buf, int val);
static void tb_percent_100th_desc(TextBuffer *buf, int val);
static void tb_dice_desc(TextBuffer *buf, int val);
static void tb_mult_desc(TextBuffer *buf, int val);
static void tb_bool_desc(TextBuffer *buf, int val);
#if 0
static void append_number(TextBuffer *buf, int value, int dflt);
#endif
static void append_help_phrase(TextBuffer *buf, char *phrase);
static void append_notes(TextBuffer *buf, Obj *notes);

/* The first help node in the chain. */

HelpNode *first_help_node;

/* The last help node. */

HelpNode *last_help_node;

/* The help node with copying and copyright info. */

HelpNode *copying_help_node;

/* The help node with (non-)warranty info. */

HelpNode *warranty_help_node;

HelpNode *default_prev_help_node;

/* Create the initial help node and link it to itself.  Subsequent
   nodes will be inserted later, after a game has been loaded. */

void
init_help(void)
{
    /* Note that we can't use add_help_node to set up the first help
       node. */
    first_help_node = create_help_node();
    first_help_node->key = "help system";
    first_help_node->fn = describe_help_system;
    first_help_node->prev = first_help_node->next = first_help_node;
    last_help_node = first_help_node;
    copying_help_node =
      add_help_node("copyright", describe_copyright, 0, first_help_node);
    warranty_help_node =
      add_help_node("warranty", describe_warranty, 0, copying_help_node);
    /* Set the place for new nodes to appear normally. */
    default_prev_help_node = copying_help_node;
    add_help_node("news", describe_news, 0, NULL);
}

/* This function creates the actual set of help nodes for the kernel. */

void
create_game_help_nodes(void)
{
    int u, m, t, a;
    char *name, *longname;
    HelpNode *node;

    add_help_node("instructions", describe_instructions, 0, NULL);
    add_help_node("game overview", describe_game_design, 0, NULL);
    add_help_node("scoring", describe_scorekeepers, 0, NULL);
    add_help_node("modules", describe_game_modules, 0, NULL);
    add_help_node("game setup", describe_setup, 0, NULL);
    add_help_node("world", describe_world, 0, NULL);
    for_all_unit_types(u) {
	longname = u_long_name(u);
	if (!empty_string(longname)) {
	    sprintf(spbuf, "%s (%s)", longname, u_type_name(u));
	    name = copy_string(spbuf);
	} else {
	    name = u_type_name(u);
	}
	node = add_help_node(name, describe_utype, u, NULL);
	node->nclass = utypenode;
    }
    for_all_material_types(m) {
	node = add_help_node(m_type_name(m), describe_mtype, m, NULL);
	node->nclass = mtypenode;
    }
    for_all_terrain_types(t) {
	node = add_help_node(t_type_name(t), describe_ttype, t, NULL);
	node->nclass = ttypenode;
    }
    for_all_advance_types(a) {
	node = add_help_node(a_type_name(a), describe_atype, a, NULL);
	node->nclass = atypenode;
    }
    add_help_node("general concepts", describe_concepts, 0, NULL);
    /* Invalidate any existing topics node. */
    first_help_node->text = NULL;
}

/* Create an empty help node. */

HelpNode *
create_help_node(void)
{
    HelpNode *node = (HelpNode *) xmalloc(sizeof(HelpNode));

    node->key = NULL;
    node->fn = NULL;
    node->nclass = miscnode;
    node->arg = 0;
    node->text = NULL;
    node->prev = node->next = NULL;
    return node;
}

/* Add a help node after the given node. */

HelpNode *
add_help_node(char *key, void (*fn)(int t, char *key, TextBuffer *buf),
	      int arg, HelpNode *prevnode)
{
    HelpNode *node, *nextnode;

    if (empty_string(key)) {
	run_error("empty help key");
    }
    node = create_help_node();
    node->key = key;
    node->fn = fn;
    node->arg = arg;
    if (prevnode == NULL)
      prevnode = default_prev_help_node->prev;
    nextnode = prevnode->next;
    node->prev = prevnode;
    node->next = nextnode;
    prevnode->next = node;
    nextnode->prev = node;
    /* Might need to fix last help node. */
    last_help_node = first_help_node->prev;
    return node;
}

/* Given a string and node, find the next node whose key matches. */

HelpNode *
find_help_node(HelpNode *node, char *str)
{
    HelpNode *tmp;

    /* Note that the search wraps around. */
    for (tmp = node->next; tmp != node; tmp = tmp->next) {
    	if (strcmp(tmp->key, str) == 0)
    	  return tmp;
    	if (strstr(tmp->key, str) != NULL)
    	  return tmp;
    }
    return NULL;
}

/* Return the string containing the text of the help node, possibly
   computing it first. */

char *
get_help_text(HelpNode *node)
{
    TextBuffer tbuf;

    if (node != NULL) {
	/* Maybe calculate the text to display. */
	if (node->text == NULL) {
	    if (node->fn != NULL) {
		/* (should allow for variable-size allocation) */
	    	obstack_begin(&(tbuf.ostack), 200);
		if (1) {
		    node->textend = 0;
		    (*(node->fn))(node->arg, node->key, &tbuf);
		    obstack_1grow(&(tbuf.ostack), '\0');
		    node->text = copy_string(obstack_finish(&(tbuf.ostack)));
		    obstack_free(&(tbuf.ostack), 0);
		    node->textend = strlen(node->text);
		} else {
		    /* Ran out of memory... (would never get here though!) */
		}
	    } else {
		/* Generate a default message if nothing to compute help. */
		sprintf(spbuf, "%s: No info available.", node->key);
		node->text = copy_string(spbuf);
		node->textend = strlen(node->text);
	    }
	    Dprintf("Size of help node \"%s\" text is %d\n", node->key, node->textend);
	}
	return node->text;
    } else {
	return NULL;
    }
}

static void
describe_help_system(int arg, char *key, TextBuffer *buf)
{
    tbcat(buf, "This is the header node of the Xconq help system.\n");
    tbcat(buf, "Go forward or backward from here to see the online help.\n");
}

/* Create a raw list of help topics by just iterating through all the nodes,
   except for the topics node itself. */

void
describe_topics(int arg, char *key, TextBuffer *buf)
{
    HelpNode *topics, *tmp;

    topics = find_help_node(first_help_node, "topics");
    /* Unlikely that we'll call this without the topics node existing
       already, but just in case... */
    if (topics == NULL)
      return;
    for (tmp = topics->next; tmp != topics; tmp = tmp->next) {
	tbprintf(buf, "%s", tmp->key);
	tbcat(buf, "\n");
    }
}

/* Get the news file and put it into text buffer. */

static void
describe_news(int arg, char *key, TextBuffer *buf)
{
    FILE *fp;

    fp = open_file(news_filename(), "r");
    if (fp != NULL) {
	tbcat(buf, "XCONQ NEWS\n\n");
	while (fgets(spbuf, BUFSIZE-1, fp) != NULL) {
	    tbcat(buf, spbuf);
	}
	fclose(fp);
    } else {
	tbcat(buf, "(no news)");
    }
}

/* Describe general game concepts in a general way.  If a concept does
   not apply to the game in effect, then just say it's not part of
   this game (would be confusing if the online doc described things
   irrelevant to the specific game). */

static void
describe_concepts(int arg, char *key, TextBuffer *buf)
{
    tbcat(buf, "Hit points (HP) represent the overall condition of ");
    tbcat(buf, "the unit.");
    tbcat(buf, "\n");
    tbcat(buf, "Action points (ACP) are what a unit needs to be able ");
    tbcat(buf, "to do anything at all.  Typically a unit will use 1 ACP ");
    tbcat(buf, "to move 1 cell.");
    tbcat(buf, "\n");
    tbcat(buf, "Movement points (MP) represent varying costs of movement ");
    tbcat(buf, "actions, such as a difficult-to-cross border.  The number ");
    tbcat(buf, "of movement points is added up then divided by unit's speed ");
    tbcat(buf, "to get the total number of acp used up by a move.");
    tbcat(buf, "\n");
    if (0) {
    } else {
	tbcat(buf, "No combat experience (CXP) in this game.\n");
    }
    if (0) {
    } else {
	tbcat(buf, "No morale (MO) in this game.\n");
    }
    tbcat(buf, "Each unit that can do anything has a plan, and a list of ");
    tbcat(buf, "tasks to perform.\n");
    /* (should describe more general concepts) */
}

static void
describe_instructions(int arg, char *key, TextBuffer *buf)
{
    Obj *instructions = mainmodule->instructions;

    if (instructions != lispnil) {
	append_notes(buf, instructions);
    } else {
	tbcat(buf, "(no instructions supplied)");
    }
}

/* Spit out all the general game_design parameters in a readable fashion. */

static void
describe_game_design(int arg, char *key, TextBuffer *buf)
{
    int u, m, t, a;
    
    /* Replicate title and blurb? (should put title at head of
       windows, and pages if printed) */
    tbprintf(buf, "*** %s ***\n",
	     (mainmodule->title ? mainmodule->title : mainmodule->name));
    tbcat(buf, "\n");
    tbprintf(buf, "This game includes %d unit types and %d terrain types",
	     numutypes, numttypes);
    if (nummtypes > 0) {
	tbprintf(buf, ", along with %d material types", nummtypes);
    }
    if (numatypes > 0) {
	tbprintf(buf, ", and it has %d types of advances", numatypes);
    }
    tbcat(buf, ".\n");
    if (g_sides_min() == g_sides_max()) {
    	tbprintf(buf, "Exactly %d sides may play.\n", g_sides_min());
    } else {
    	tbprintf(buf, "From %d up to %d sides may play.\n",
		 g_sides_min(), g_sides_max());
    }
    tbcat(buf, "\n");
    if (g_advantage_min() == g_advantage_max())
      tbprintf(buf, "Player advantages are fixed.\n");
    else
      tbprintf(buf, "Player advantages may range from %d to %d, defaulting to %d.\n",
	       g_advantage_min(), g_advantage_max(), g_advantage_default());
    tbcat(buf, "\n");
    if (g_see_all()) {
	tbcat(buf, "Everything is always seen by all sides.\n");
    } else {
    	if (g_see_terrain_always()) {
	    tbcat(buf, "Terrain view is always accurate once seen.\n");
    	}
    	/* (should only have if any weather to be seen) */
    	if ((any_temp_variation || any_wind_variation || any_clouds)
	    && g_see_weather_always()) {
	    tbcat(buf, "Weather view is always accurate once terrain seen.\n");
    	}
    	if (g_terrain_seen()) {
	    tbcat(buf, "World terrain is already seen by all sides.\n");
    	}
    }
    tbcat(buf, "\n");
    if (g_last_turn() < 9999) {
	tbprintf(buf, "Game can go for up to %d turns", g_last_turn());
	if (g_extra_turn() > 0) {
	    tbprintf(buf, ", with %d%% chance of additional turn thereafter.",
		     g_extra_turn());
	}
	tbcat(buf, ".\n");
    }
    if (g_rt_for_game() > 0) {
	tbprintf(buf, "Entire game can last up to %d minutes.\n",
		g_rt_for_game() / 60);
    }
    if (g_rt_per_turn() > 0) {
	tbprintf(buf, "Each turn can last up to %d minutes.\n",
		g_rt_per_turn() / 60);
    }
    if (g_rt_per_side() > 0) {
	tbprintf(buf, "Each side gets a total %d minutes to act.\n",
		g_rt_per_side() / 60);
    }
    if (g_units_in_game_max() >= 0) {
	tbprintf(buf, "Limited to no more than %d units in all.\n",
		 g_units_in_game_max());
    }
    if (g_units_per_side_max() >= 0) {
	tbprintf(buf, "Limited to no more than %d units per side.\n",
		 g_units_per_side_max());
    }
    if (g_use_side_priority()) {
	tbcat(buf, "Sides move sequentially, in priority order.\n");
    } else {
	tbcat(buf, "Sides move simultaneously.\n");
    }
    if (any_temp_variation) {
	tbprintf(buf, "Lowest possible temperature is %d, at an elevation of %d.\n",
		 g_temp_floor(), g_temp_floor_elev());
	tbprintf(buf, "Temperatures averaged to range %d.\n",
		 g_temp_mod_range());
    }
    tbcat(buf, "\nUnit Types:\n");
    for_all_unit_types(u) {
	tbprintf(buf, "  %s", u_type_name(u));
	if (!empty_string(u_help(u)))
	  tbprintf(buf, " (%s)", u_help(u));
	tbcat(buf, "\n");
#ifdef DESIGNERS
	/* Show designers a bit more. */
	if (numdesigners > 0) {
	    tbcat(buf, "    [");
	    if (!empty_string(u_uchar(u)))
	      tbprintf(buf, "char '%s'", u_uchar(u));
	    else
	      tbcat(buf, "no char");
	    if (!empty_string(u_gchar(u)))
	      tbprintf(buf, "generic char '%s'", u_gchar(u));
	    else
	      tbcat(buf, "no generic char");
	    if (!empty_string(u_image_name(u)))
	      tbprintf(buf, ", image \"%s\"", u_image_name(u));
	    if (!empty_string(u_generic_name(u)))
	      tbprintf(buf, ", generic name \"%s\"", u_generic_name(u));
	    if (u_desc_format(u) != lispnil) {
	        tbcat(buf, ", special format");
	    }
	    tbcat(buf, "]\n");
	}
#endif /* DESIGNERS */
    }
    tbcat(buf, "\nTerrain Types:\n");
    for_all_terrain_types(t) {
	tbprintf(buf, "  %s", t_type_name(t));
	if (!empty_string(t_help(t)))
	  tbprintf(buf, " (%s)", t_help(t));
	tbcat(buf, "\n");
#ifdef DESIGNERS
	/* Show designers a bit more. */
	if (numdesigners > 0) {
	    tbcat(buf, "    [");
	    if (!empty_string(t_char(t)))
	      tbprintf(buf, "char '%s'", t_char(t));
	    else
	      tbcat(buf, "no char");
	    if (!empty_string(t_image_name(t)))
	      tbprintf(buf, ", image \"%s\"", t_image_name(t));
	    tbcat(buf, "]\n");
	}
#endif /* DESIGNERS */
    }
    if (nummtypes > 0) {
	tbcat(buf, "\nMaterial Types:\n");
	for_all_material_types(m) {
	    tbprintf(buf, "  %s", m_type_name(m));
	    if (!empty_string(m_help(m)))
	      tbprintf(buf, " (%s)", m_help(m));
	    tbcat(buf, "\n");
#ifdef DESIGNERS
	    /* Show designers a bit more. */
	    if (numdesigners > 0) {
		tbcat(buf, "    [");
		if (!empty_string(m_char(m)))
		  tbprintf(buf, "char '%s'", m_char(m));
		else
		  tbcat(buf, "no char");
		if (!empty_string(m_image_name(m)))
		  tbprintf(buf, ", image \"%s\"", m_image_name(m));
		tbcat(buf, "]\n");
	    }
#endif /* DESIGNERS */
	}
    }
    if (numatypes > 0) {
	tbcat(buf, "\nAdvances:\n");
	for_all_advance_types(a) {
	    tbprintf(buf, "  %s", a_type_name(a));
	    if (!empty_string(a_help(a)))
	      tbprintf(buf, " (%s)", a_help(a));
	    tbcat(buf, "\n");
#ifdef DESIGNERS
	    /* Show designers a bit more. */
	    if (numdesigners > 0) {
		tbcat(buf, "    [");
		if (!empty_string(a_image_name(a)))
		  tbprintf(buf, ", image \"%s\"", a_image_name(a));
		tbcat(buf, "]\n");
	    }
#endif /* DESIGNERS */
	}
    }
#ifdef DESIGNERS
    /* Show designers a bit more. */
    if (numdesigners > 0) {
	tbcat(buf, "FOR DESIGNERS:\n");
	tbprintf(buf, "Unseen terrain char is \"%s\".\n", g_unseen_char());
	tbprintf(buf, "Scorefile name is \"%s\".\n", g_scorefile_name());
    }
#endif /* DESIGNERS */
}

/* Display game module info to a side. */

static void
describe_game_modules(int arg, char *key, TextBuffer *buf)
{
    if (mainmodule != NULL) {
	/* First put out basic module info. */
	describe_game_module_aux(buf, mainmodule, 0);
	/* Now do the lengthy module notes (with no indentation). */
	describe_module_notes(buf, mainmodule);
    } else {
	tbcat(buf, "(No game module information is available.)");
    }
}

/* Recurse down through included modules to display docs on each.
   Indents each file by inclusion level.  Note that modules cannot
   be loaded more than once, so each will be described only once here. */

static void   
describe_game_module_aux(TextBuffer *buf, Module *module, int level)
{
    int i;
    char indentbuf[100];
    char dashbuf[100];
    Module *submodule;

    dashbuf[0] = '\0';
    indentbuf[0] = '\0';
    for (i = 0; i < level; ++i) {
	strcat(dashbuf,   "-- ");
	strcat(indentbuf, "   ");
    }
    tbprintf(buf, "%s\"%s\"", dashbuf,
	    (module->title ? module->title : module->name));
    /* Display the true name of the module if not the same as the title. */
    if (module->title != NULL && strcmp(module->title, module->name) != 0) {
	tbprintf(buf, " (\"%s\")", module->name);
    }
    if (module->version != NULL) {
	tbprintf(buf, " (version \"%s\")", module->version);
    }
    tbcat(buf, "\n");
    tbprintf(buf, "          %s\n",
	    (module->blurb ? module->blurb : "(no description)"));
    if (module->notes != lispnil) {
	tbprintf(buf, "Notes to \"%s\":\n", module->name);
	append_notes(buf, module->notes);
	tbprintf(buf, "\n\n");
    }
#if 0
    if (module->notes != lispnil) {
	tbprintf(buf, "%s          (See notes below)\n", indentbuf);
    }
#endif
    /* Now describe any included modules. */
    for_all_includes(module, submodule) {
	describe_game_module_aux(buf, submodule, level + 1);
    }
}

/* Dump the module designer's notes into the given buffer.  When doing
   submodules, don't indent. */

static void
describe_module_notes(TextBuffer *buf, Module *module)
{
    Module *submodule;

#ifdef DESIGNERS
    /* Only show design notes if any designers around. */
    if (numdesigners > 0 && module->designnotes != lispnil) {
	tbprintf(buf, "\nDesign Notes to \"%s\":\n", module->name);
	append_notes(buf, module->designnotes);
    }
#endif /* DESIGNERS */
    for_all_includes(module, submodule) {
	describe_module_notes(buf, submodule);
    }
}

int
any_ut_capacity_x(int u)
{
    int t;
	
    for_all_terrain_types(t) {
	if (ut_capacity_x(u, t) != 0)
	  return TRUE;
    }
    return FALSE;
}

int
any_mp_to_enter_unit(int u)
{
    int u2;
	
    for_all_unit_types(u2) {
	if (uu_mp_to_enter(u, u2) != 0)
	  return TRUE;
    }
    return FALSE;
}

int
any_mp_to_leave_unit(int u)
{
    int u2;
	
    for_all_unit_types(u2) {
	if (uu_mp_to_leave(u, u2) != 0)
	  return TRUE;
    }
    return FALSE;
}

int
any_enter_indep(int u)
{
    int u2;
	
    for_all_unit_types(u2) {
	if (uu_can_enter_indep(u, u2))
	  return TRUE;
    }
    return FALSE;
}

/* Full details on the given type of unit. */

/* (The defaults should come from the *.def defaults!!) */

static void
describe_utype(int u, char *key, TextBuffer *buf)
{
    char sidetmpbuf[BUFSIZE];
    int m, first, speedvaries, usesm, a;
    Side *side;

    append_help_phrase(buf, u_help(u));
    if (u_point_value(u) > 0) {
	tbprintf(buf, "     (point value %d)\n", u_point_value(u));
    }
    /* Display the designer's notes for this type. */
    if (u_notes(u) != lispnil) {
	tbcat(buf, "Notes:\n");
	append_notes(buf, u_notes(u));
	tbcat(buf, "\n\n");
    }
    if (u_can_be_self(u)) {
    	tbcat(buf, "Can be self-unit");
    	if (u_self_changeable(u))
	  tbcat(buf, "; side may choose another to be self-unit");
    	if (u_self_resurrects(u))
	  tbcat(buf, "; if dies, another becomes self-unit");
	if (g_self_required())
	  tbcat(buf, " (must always have a self-unit during game)");
    	tbcat(buf, ".\n");
    }
    if (u_possible_sides(u) != lispnil) {
	tbcat(buf, "Possible sides in this game: ");
	first = TRUE;
	for_all_sides(side) {
	    if (type_allowed_on_side(u, side)) {
		if (first)
		  first = FALSE;
		else
		  tbcat(buf, ", ");
		tbcat(buf, shortest_side_title(side, sidetmpbuf));
	    }
    	}
    	tbcat(buf, ".\n");
    }
    if (u_type_in_game_max(u) >= 0) {
	tbprintf(buf, "At most %d allowed in a game.\n", u_type_in_game_max(u));
    }
    if (u_type_per_side_max(u) >= 0) {
	tbprintf(buf, "At most %d allowed on each side in a game.\n",
		 u_type_per_side_max(u));
    }
    if (u_acp(u) > 0) {
	tbprintf(buf, "Gets %d action point%s (ACP) each turn",
		 u_acp(u), (u_acp(u) == 1 ? "" : "s"));
	if (u_acp_min(u) != 0) {
	    tbprintf(buf, ", can go down to %d ACP", u_acp_min(u));
	}
	if (u_acp_max(u) != -1) {
	    tbprintf(buf, ", can go up to %d ACP", u_acp_max(u));
	}
	if (u_free_acp(u) != 0) {
	    tbprintf(buf, ", %d free", u_free_acp(u));
	}
	tbcat(buf, ".\n");
	if (uu_table_row_not_default(u, uu_acp_occ_effect, 100)) {
	    tbcat(buf, "  Effect on transport's ACP: ");
	    uu_table_row_desc(buf, u, uu_acp_occ_effect, tb_mult_desc, NULL);
	    tbcat(buf, ".\n");
	}
	if (ut_table_row_not_default(u, ut_acp_night_effect, 100)) {
	    tbcat(buf, "  Night effect on ACP: ");
	    ut_table_row_desc(buf, u, ut_acp_night_effect, tb_mult_desc, "in");
	    tbcat(buf, ".\n");
	}
    } else {
	tbcat(buf, "Does not act.\n");
    }
    if (!u_direct_control(u)) {
	tbcat(buf, "Cannot be controlled directly by side.\n");
    }
    if (u_speed(u) > 0) {
	if (u_speed(u) != 100) {
	    tbcat(buf, "Normal speed (MP/ACP ratio) is ");
	    tb_fraction_desc(buf, u_speed(u));
	    tbcat(buf, ".\n");
	}
	speedvaries = FALSE;
	if (u_speed_wind_effect(u) != lispnil) {
	    /* (should add mech to describe in detail) */
	    tbcat(buf, "Wind affects speed.\n");
	    speedvaries = TRUE;
	}
	if (u_speed_damage_effect(u) != lispnil) {
	    /* (should add mech to describe in detail) */
	    tbcat(buf, "Damage affects speed.\n");
	    speedvaries = TRUE;
	}
	/* (should only list variation limits if actually needed to clip) */
	if (speedvaries) {
	    tbcat(buf, "Speed variation limited to between ");
	    tb_fraction_desc(buf, u_speed_min(u));
	    tbcat(buf, " and ");
	    tb_fraction_desc(buf, u_speed_max(u));
	    tbcat(buf, ".\n");
	}
	tbcat(buf, "MP to enter cell: ");
	ut_table_row_desc(buf, u, ut_mp_to_enter, NULL, NULL);
	tbcat(buf, ".\n");
	if (ut_table_row_not_default(u, ut_mp_to_leave, 0)) {
	    tbcat(buf, "MP to leave cell: ");
	    ut_table_row_desc(buf, u, ut_mp_to_leave, NULL, NULL);
	    tbcat(buf, ".\n");
	}
	if (any_mp_to_enter_unit(u)) {
	    tbcat(buf, "MP to enter unit: ");
	    uu_table_row_desc(buf, u, uu_mp_to_enter, NULL, NULL);
	    tbcat(buf, ".\n");
	}
	if (any_mp_to_leave_unit(u)) {
	    tbcat(buf, "MP to leave unit: ");
	    uu_table_row_desc(buf, u, uu_mp_to_leave, NULL, NULL);
	    tbcat(buf, ".\n");
	}
	if (any_enter_indep(u)) {
	    tbcat(buf, "Can enter indep unit: ");
	    uu_table_row_desc(buf, u, uu_can_enter_indep, NULL, NULL);
	    tbcat(buf, ".\n");
	}
	if (u_mp_to_leave_world(u) >= 0) {
	    tbprintf(buf, "%d MP to leave the world entirely.\n", u_mp_to_leave_world(u));
	}
	if (u_free_mp(u) > 0) {
	    tbprintf(buf, "Gets up to %d free MP if needed to move.\n", u_free_mp(u));
	}
	if (u_acp_to_move(u) > 0) {
	    tbprintf(buf, "Uses %d ACP to move.\n", u_acp_to_move(u));
	} else {
	    tbcat(buf, "Cannot move by self.\n");
	}
    } else {
	tbcat(buf, "Does not move.\n");
    }
    tbprintf(buf, "Hit Points (HP): %d.", u_hp_max(u));
    if (u_parts(u) > 1) {
	tbprintf(buf, "  Parts: %d.", u_parts(u));
    }
    if (u_hp_recovery(u) != 0) {
	tbprintf(buf, "  Recovers by ");
	tb_fraction_desc(buf, u_hp_recovery(u));
	tbprintf(buf, " HP each turn");
	if (u_hp_to_recover(u) > 0)
	  tbprintf(buf, ", if over %d HP", u_hp_to_recover(u));
	tbcat(buf, ".");
    }
    tbcat(buf, "\n");
    if (ut_table_row_not_default(u, ut_vanishes_on, 0)) {
	tbprintf(buf, "Vanishes if in: ");
	ut_table_row_desc(buf, u, ut_vanishes_on, tb_bool_desc, "");
	tbcat(buf, ".\n");
    }
    if (ut_table_row_not_default(u, ut_wrecks_on, 0)) {
	tbprintf(buf, "Immediate wreck if in: ");
	ut_table_row_desc(buf, u, ut_vanishes_on, tb_bool_desc, "");
	tbcat(buf, ".\n");
    }
    /* Describe unit's transport capabilities. */
    if (u_capacity(u) > 0
	|| uu_table_row_not_default(u, uu_capacity_x, 0)) {
	if (u_capacity(u) > 0) {
	    tbprintf(buf, "Generic capacity for units is %d.\n",
		     u_capacity(u));
	    if (uu_table_column_not_default(u, uu_size, 1)) {
		tbcat(buf, "Relative sizes of occupants: ");
		uu_table_column_desc(buf, u, uu_size, NULL, NULL);
		tbcat(buf, ".\n");
	    }
	}
	if (uu_table_row_not_default(u, uu_capacity_x, 0)) {
	    tbcat(buf, "Dedicated space for units: ");
	    uu_table_row_desc(buf, u, uu_capacity_x, NULL, NULL);
	    tbcat(buf, ".\n");
	}
	/* (should only display if < capacities would allow?) */
	if (uu_table_row_not_default(u, uu_occ_max, -1)) {
	    tbcat(buf, "Maximum number of occupants: ");
	    uu_table_row_desc(buf, u, uu_occ_max, NULL, NULL);
	    tbcat(buf, ".\n");
	}
	if (u_occ_total_max(u) >= 0) {
	    tbprintf(buf, "Maximum total of %d for all types together.\n",
		     u_occ_total_max(u));
	}
    }
    if (any_ut_capacity_x(u)) {
	tbcat(buf, "Exclusive terrain capacity: ");
        ut_table_row_desc(buf, u, ut_capacity_x, NULL, NULL);
	tbcat(buf, ".\n");
    }
    if (ut_table_row_not_default(u, ut_capacity_neg, 0)) {
	tbcat(buf, "Presence in cell negates connection capacity: ");
        ut_table_row_desc(buf, u, ut_capacity_neg, tb_bool_desc, NULL);
	tbcat(buf, ".\n");
    }
    if (uu_table_row_not_default(u, uu_zoc_range, 0)) {
	tbcat(buf, "Exerts ZOC out to: ");
        uu_table_row_desc(buf, u, uu_zoc_range, NULL, NULL);
	tbcat(buf, ".\n");
	if (ut_table_row_not_default(u, ut_zoc_into, 1)) {
	    tbcat(buf, "Exerts ZOC into: ");
	    ut_table_row_desc(buf, u, ut_zoc_into, tb_bool_desc, "in");
	    tbcat(buf, ".\n");
	}
	if (ut_table_row_not_default(u, ut_zoc_from_terrain, 100)) {
	    tbcat(buf, "Effect of own terrain: ");
	    ut_table_row_desc(buf, u, ut_zoc_from_terrain, tb_mult_desc, "in");
	    tbcat(buf, ".\n");
	}
	if (uu_table_row_not_default(u, uu_mp_to_enter_zoc, -1)) {
	    tbcat(buf, "MP to enter ZOC: ");
	    uu_table_row_desc(buf, u, uu_mp_to_enter_zoc, NULL, NULL);
	    tbcat(buf, ".\n");
	}
	if (uu_table_row_not_default(u, uu_mp_to_leave_zoc, 0)) {
	    tbcat(buf, "MP to leave ZOC: ");
	    uu_table_row_desc(buf, u, uu_mp_to_leave_zoc, NULL, NULL);
	    tbcat(buf, ".\n");
	}
	if (uu_table_row_not_default(u, uu_mp_to_traverse_zoc, 0)) {
	    tbcat(buf, "MP to traverse ZOC: ");
	    uu_table_row_desc(buf, u, uu_mp_to_traverse_zoc, NULL, NULL);
	    tbcat(buf, ".\n");
	}
    }
    if (u_cxp_max(u) != 0) {
	tbprintf(buf, "Combat experience (CXP) maximum: %d.\n", u_cxp_max(u));
    }
    if (u_morale_max(u) != 0) {
	tbprintf(buf, "Morale maximum: %d", u_morale_max(u));
	if (u_morale_recovery(u) != 0)
	  tbprintf(buf, ", recover %s each turn", u_morale_recovery(u));
	tbcat(buf, ".\n");
    }
    if (u_cp(u) != 1) {
	tbprintf(buf, "Construction points (CP): %d.\n", u_cp(u));
    }
    if (u_tech_to_see(u) != 0) {
	tbprintf(buf, "Tech to see: %d.\n", u_tech_to_see(u));
    }
    if (u_tech_to_own(u) != 0) {
	tbprintf(buf, "Tech to own: %d.\n", u_tech_to_own(u));
    }
    if (u_tech_to_use(u) != 0) {
	tbprintf(buf, "Tech to use: %d.\n", u_tech_to_use(u));
    }
    if (u_tech_to_build(u) != 0) {
	tbprintf(buf, "Tech to build: %d.\n", u_tech_to_build(u));
    }
    if (u_tech_max(u) != 0) {
	tbprintf(buf, "Tech max: %d.\n", u_tech_max(u));
    }
    if (u_tech_max(u) != 0 && u_tech_per_turn_max(u) != PROPHI) {
	tbprintf(buf, "Tech increase per turn max: %d.\n", u_tech_per_turn_max(u));
    }
    if (u_tech_from_ownership(u) != 0) {
	tbprintf(buf, "Tech guaranteed by ownership: %d.\n", u_tech_from_ownership(u));
    }
    if (u_tech_leakage(u) != 0) {
	tbprintf(buf, "Tech leakage: %d.\n", u_tech_leakage(u));
    }
    if (u_acp(u) > 0
        && type_can_develop(u) > 0
        ) {
        tbcat(buf, "\nDevelop:\n");
	tbcat(buf, "ACP to develop: ");
        uu_table_row_desc(buf, u, uu_acp_to_develop, NULL, NULL);
	tbcat(buf, ".\n");
	tbcat(buf, "  Tech gained: ");
        uu_table_row_desc(buf, u, uu_tech_per_develop, tb_fraction_desc, NULL);
	tbcat(buf, ".\n");
    }
    if (u_acp(u) > 0
        && (type_can_create(u) > 0
            || type_can_complete(u) > 0
        )) {
        tbcat(buf, "\nConstruction:\n");
        if (type_can_create(u) > 0) {
	    tbcat(buf, "ACP to create: ");
	    uu_table_row_desc(buf, u, uu_acp_to_create, NULL, NULL);
	    tbcat(buf, ".\n");
	    if (uu_table_row_not_default(u, uu_create_range, 0)) {
  		tbcat(buf, "  Creation distance max: ");
		uu_table_row_desc(buf, u, uu_create_range, NULL, NULL);
 		tbcat(buf, ".\n");
 	    }
	    if (uu_table_row_not_default(u, uu_creation_cp, 1)) {
  		tbcat(buf, "  CP upon creation: ");
		uu_table_row_desc(buf, u, uu_creation_cp, NULL, NULL);
 		tbcat(buf, ".\n");
 	    }
	}
        if (type_can_complete(u) > 0) {
	    tbcat(buf, "ACP to build: ");
	    uu_table_row_desc(buf, u, uu_acp_to_build, NULL, NULL);
	    tbcat(buf, ".\n");
	    if (uu_table_row_not_default(u, uu_cp_per_build, 1)) {
 		tbcat(buf, "  CP added per build: ");
 		uu_table_row_desc(buf, u, uu_cp_per_build, NULL, NULL);
 		tbcat(buf, ".\n");
 	    }
        }
        if (u_cp_per_self_build(u) > 0) {
	    tbprintf(buf, "Can finish building self at %d cp, will add %d cp per action.\n",
		    u_cp_to_self_build(u), u_cp_per_self_build(u));
        }
	if (uu_table_row_not_default(u, uu_build_range, 0)) {
	    tbcat(buf, "Range at which can build: ");
	    uu_table_row_desc(buf, u, uu_build_range, NULL, NULL);
	    tbcat(buf, ".\n");
	} else {
	    tbcat(buf, "Can build at own location.\n");
	}
        /* Toolup help. */
        if (type_can_toolup(u)) {
	    tbcat(buf, "ACP to toolup: ");
	    uu_table_row_desc(buf, u, uu_acp_to_toolup, NULL, NULL);
	    tbcat(buf, ".\n");
	    tbcat(buf, "  TP/toolup action: ");
	    uu_table_row_desc(buf, u, uu_tp_per_toolup, NULL, NULL);
	    tbcat(buf, ".\n");
	    /* (should put these with type beING built...) */
	    tbcat(buf, "  TP to build: ");
	    uu_table_row_desc(buf, u, uu_tp_to_build, NULL, NULL);
	    tbcat(buf, ".\n");
	    tbcat(buf, "  TP max: ");
	    uu_table_row_desc(buf, u, uu_tp_max, NULL, NULL);
	    tbcat(buf, ".\n");
        }
        
    }
    if ((u_acp(u) > 0
	 && (type_can_attack(u) > 0
	     || u_acp_to_fire(u) > 0
	     || type_can_capture(u) > 0
	    ))
	|| may_detonate(u)
	|| uu_table_row_not_default(u, uu_protection, 100)
	|| uu_table_row_not_default(u, uu_retreat_chance, 0)
	|| uu_table_row_not_default(u, uu_acp_retreat, 0)
	|| u_wrecked_type(u) != NONUTYPE
	) {
	tbcat(buf, "\nCombat:\n");
	if (type_can_attack(u) > 0) {
	    tbcat(buf, "Can attack (ACP ");
	    uu_table_row_desc(buf, u, uu_acp_to_attack, NULL, "vs");
	    tbcat(buf, ").\n");
	    if (uu_table_row_not_default(u, uu_attack_range, 1)) {
		tbcat(buf, "Attack range is ");
		uu_table_row_desc(buf, u, uu_attack_range, NULL, "vs");
		tbcat(buf, ".\n");
		tbcat(buf, "Attack range min is ");
		uu_table_row_desc(buf, u, uu_attack_range_min, NULL, "vs");
		tbcat(buf, ".\n");
	    }
	}
   	if (u_acp_to_fire(u) > 0) {
	    tbprintf(buf, "Can fire (%d ACP), at ranges", u_acp_to_fire(u));
	    if (u_range_min(u) > 0) {
		tbprintf(buf, " from %d", u_range_min(u));
	    }
	    tbprintf(buf, " up to %d", u_range(u));
	    tbcat(buf, ".\n");
	}
	tbcat(buf, "Hit chances are ");
	uu_table_row_desc(buf, u, uu_hit, tb_percent_desc, "vs");
	tbcat(buf, ".\n");
   	if (u_acp_to_fire(u) > 0) {
	    if (uu_table_row_not_default(u, uu_fire_hit, -1)) {
		tbcat(buf, "Hit chances if firing are ");
		uu_table_row_desc(buf, u, uu_fire_hit, tb_percent_desc, "vs");
		tbcat(buf, ".\n");
	    } else {
		tbcat(buf, "Hit chances if firing same as for regular combat.\n");
	    }
   	}
	if (ut_table_row_not_default(u, ut_attack_terrain_effect, 100)) {
	    tbcat(buf, "Effect of attacker's terrain is ");
	    ut_table_row_desc(buf, u, ut_attack_terrain_effect,
			      tb_mult_desc, "in");
	    tbcat(buf, ".\n");
	}
	tbcat(buf, "Damage is ");
	uu_table_row_desc(buf, u, uu_damage, tb_dice_desc, "vs");
	tbcat(buf, ".\n");
	if (uu_table_row_not_default(u, uu_tp_damage, 0)) {
	    tbcat(buf, "Tooling damage is ");
	    uu_table_row_desc(buf, u, uu_tp_damage, tb_dice_desc, NULL);
	    tbcat(buf, ".\n");
	}
   	if (u_acp_to_fire(u) > 0) {
	    if (uu_table_row_not_default(u, uu_fire_damage, -1)) {
		tbcat(buf, "Damage if firing is ");
		uu_table_row_desc(buf, u, uu_fire_damage, NULL, "vs");
		tbcat(buf, ".\n");
	    } else {
		tbcat(buf, "Damages if firing same as for regular combat.\n");
	    }
	}
	if (uu_table_row_not_default(u, uu_acp_to_defend, 1)) {
	    tbcat(buf, "If attacked, ACP to defend is ");
	    uu_table_row_desc(buf, u, uu_acp_to_defend, NULL, "vs");
	    tbcat(buf, ".\n");
	}
	if (ut_table_row_not_default(u, ut_defend_terrain_effect, 100)) {
	    tbcat(buf, "If attacked, effect of own terrain is ");
	    ut_table_row_desc(buf, u, ut_defend_terrain_effect,
			      tb_mult_desc, "in");
	    tbcat(buf, ".\n");
	}
	if (um_table_row_not_default(u, um_hit_by, 0)) {
	    tbcat(buf, "Ammo needed to hit unit: ");
	    um_table_row_desc(buf, u, um_hit_by, NULL);
	    tbcat(buf, ".\n");
	}
	if (type_can_capture(u) > 0) {
	    tbcat(buf, "Can capture (ACP ");
	    uu_table_row_desc(buf, u, uu_acp_to_capture, NULL, "vs");
	    tbcat(buf, ").\n");
	    tbcat(buf, "Chance to capture: ");
	    uu_table_row_desc(buf, u, uu_capture, tb_percent_desc, "vs");
	    tbcat(buf, ".\n");
	    if (uu_table_row_not_default(u, uu_indep_capture, -1)) {
		tbcat(buf, "Chance to capture indep: ");
		uu_table_row_desc(buf, u, uu_indep_capture,
				  tb_percent_desc, "vs");
		tbcat(buf, ".\n");
	    }
	}
	if (may_detonate(u)) {
	    /* Display all the different ways that a unit might detonate. */
	    if (u_acp_to_detonate(u) > 0) {
		tbprintf(buf, "Can detonate self (%d ACP).\n",
			 u_acp_to_detonate(u));
	    }
	    if (u_detonate_on_death(u)) {
		tbprintf(buf,
			 "%d%% chance to detonate if destroyed in combat.\n",
			 u_detonate_on_death(u));
	    }
	    if (uu_table_row_not_default(u, uu_detonate_on_hit, 0)) {
		tbcat(buf, "Chance to detonate upon being hit: ");
		uu_table_row_desc(buf, u, uu_detonate_on_hit, tb_percent_desc,
				  NULL);
		tbcat(buf, ".\n");
	    }
	    if (uu_table_row_not_default(u, uu_detonate_on_capture, 0)) {
		tbcat(buf, "Chance to detonate upon capture: ");
		uu_table_row_desc(buf, u, uu_detonate_on_capture,
				  tb_percent_desc, NULL);
		tbcat(buf, ".\n");
	    }
	    if (uu_table_row_not_default(u, uu_detonate_approach_range, -1)) {
		tbcat(buf, "Will detonate upon approach within range: ");
		uu_table_row_desc(buf, u, uu_detonate_approach_range,
				  NULL, NULL);
		tbcat(buf, ".\n");
	    }
	    if (ut_table_row_not_default(u, ut_detonation_accident, 0)) {
		tbcat(buf, "Chance of accidental detonation: ");
		ut_table_row_desc(buf, u, ut_detonation_accident,
				  tb_percent_desc, "in");
		tbcat(buf, ".\n");
	    }
	    tbcat(buf, "Detonation damage at ground zero is ");
	    uu_table_row_desc(buf, u, uu_detonation_damage_at, NULL, NULL);
	    tbcat(buf, ".\n");
	    /* (should only display if effect range > 0) */
	    tbcat(buf, "Detonation damage to adjacent units is ");
	    uu_table_row_desc(buf, u, uu_detonation_damage_adj, NULL, NULL);
	    tbcat(buf, ".\n");
	    tbcat(buf, "Range of detonation effect on units is ");
	    uu_table_row_desc(buf, u, uu_detonation_range, NULL, NULL);
	    tbcat(buf, ".\n");
	    /* Damage decreases as inverse square of distance. */
	    if (ut_table_row_not_default(u, ut_detonation_damage, 0)) {
		tbcat(buf, "Chance of detonation damage to terrain is ");
		ut_table_row_desc(buf, u, ut_detonation_damage, NULL, NULL);
		tbcat(buf, ".\n");
		tbcat(buf, "Range of detonation effect on terrain is ");
		ut_table_row_desc(buf, u, ut_detonation_range, NULL, NULL);
		tbcat(buf, ".\n");
	    }
	    if (u_hp_per_detonation(u) < u_hp_max(u)) {
		tbprintf(buf, "Loses %d HP per detonation.\n",
			 u_hp_per_detonation(u));
	    } else {
		tbprintf(buf, "Always destroyed by detonation.\n");
	    }
	}
	if (uu_table_row_not_default(u, uu_hp_min, 0)) {
	    tbcat(buf, "Combat never reduces defender's HP below ");
	    uu_table_row_desc(buf, u, uu_hp_min, NULL, "vs");
	    tbcat(buf, ".\n");
	}
	if (uu_table_row_not_default(u, uu_protection, 100)) {
	    tbcat(buf, "Protection of occupants/transport is ");
	    uu_table_row_desc(buf, u, uu_protection, tb_mult_desc, NULL);
	    tbcat(buf, ".\n");
	}
	if (uu_table_row_not_default(u, uu_occ_combat, 100)) {
	    tbcat(buf, "Combat effectiveness as occupant is ");
	    uu_table_row_desc(buf, u, uu_occ_combat, tb_percent_desc, "in");
	    tbcat(buf, ".\n");
	}
	if (uu_table_row_not_default(u, uu_retreat_chance, 0)) {
	    tbcat(buf, "Chance to retreat from combat is ");
	    uu_table_row_desc(buf, u, uu_retreat_chance, tb_percent_desc,
			      "vs");
	    tbcat(buf, ".\n");
	}
	if (uu_table_row_not_default(u, uu_acp_retreat, 0)) {
	    tbcat(buf, "Extra ACP for retreating is ");
	    uu_table_row_desc(buf, u, uu_acp_retreat, NULL, "vs");
	    tbcat(buf, ".\n");
	}
	if (u_wrecked_type(u) != NONUTYPE) {
	    tbprintf(buf, "Becomes %s when destroyed.\n",
		     u_type_name(u_wrecked_type(u)));
	}
    }
    if (u_acp(u) > 0
        && (u_acp_to_change_side(u) > 0
            || u_acp_to_disband(u) > 0
            || u_acp_to_transfer_part(u) > 0
	    || uu_table_row_not_default(u, uu_acp_to_repair, 0)
	    || uu_table_row_not_default(u, uu_acp_to_change_type, 0)
	    || ut_table_row_not_default(u, ut_acp_to_add_terrain, 0)
	    || ut_table_row_not_default(u, ut_acp_to_remove_terrain, 0)
            )) {
	tbcat(buf, "\nOther Actions:\n");
	if (u_acp_to_change_side(u) > 0) {
	    tbprintf(buf, "Can be given to another side (%d ACP).\n",
		    u_acp_to_change_side(u));
	}
	if (u_acp_to_disband(u) > 0) {
	    tbprintf(buf, "Can be disbanded (%d ACP)", u_acp_to_disband(u));
	    if (u_hp_per_disband(u) < u_hp_max(u)) {
	    	tbprintf(buf, ", losing %d HP per action",
			 u_hp_per_disband(u));
	    }
	    tbcat(buf, ".\n");
	    if (um_table_row_not_default(u, um_supply_per_disband, 0)) {
		tbprintf(buf, "Supply yield per disband action: ");
		um_table_row_desc(buf, u, um_supply_per_disband, NULL);
		tbcat(buf, ".\n");
	    }
	    if (um_table_row_not_default(u, um_recycleable, 0)) {
		tbprintf(buf, "Additional material when unit is gone: ");
		um_table_row_desc(buf, u, um_recycleable, NULL);
		tbcat(buf, ".\n");
	    }
	}
	if (u_acp_to_transfer_part(u) > 0) {
	    tbprintf(buf, "Can transfer parts (%d ACP).\n",
		    u_acp_to_transfer_part(u));
	}
	if (uu_table_row_not_default(u, uu_acp_to_repair, 0)) {
	    tbcat(buf, "ACP to repair is ");
	    uu_table_row_desc(buf, u, uu_acp_to_repair, NULL, NULL);
	    tbcat(buf, ".\n");
	    tbcat(buf, "Repair performance is ");
	    uu_table_row_desc(buf, u, uu_repair, NULL, NULL);
	    tbcat(buf, ".\n");
	    tbcat(buf, "Min HP to repair is ");
	    uu_table_row_desc(buf, u, uu_hp_to_repair, NULL, NULL);
	    tbcat(buf, ".\n");
	}
	if (uu_table_row_not_default(u, uu_acp_to_change_type, 0)) {
	    tbcat(buf, "ACP to change type is ");
	    uu_table_row_desc(buf, u, uu_acp_to_change_type, NULL, NULL);
	    tbcat(buf, ".\n");
	}
	if (ut_table_row_not_default(u, ut_acp_to_add_terrain, 0)) {
	    tbcat(buf, "ACP to add/change terrain is ");
	    ut_table_row_desc(buf, u, ut_acp_to_add_terrain, NULL, NULL);
	    tbcat(buf, ".\n");
	    if (ut_table_row_not_default(u, ut_alter_range, 0)) {
		tbcat(buf, "  Can alter up to range ");
		ut_table_row_desc(buf, u, ut_alter_range, NULL, NULL);
		tbcat(buf, ".\n");
	    }
	}
	if (ut_table_row_not_default(u, ut_acp_to_remove_terrain, 0)) {
	    tbcat(buf, "ACP to remove terrain is ");
	    ut_table_row_desc(buf, u, ut_acp_to_remove_terrain, NULL, NULL);
	    tbcat(buf, ".\n");
	    if (ut_table_row_not_default(u, ut_alter_range, 0)) {
		tbcat(buf, "  Can alter up to range ");
		ut_table_row_desc(buf, u, ut_alter_range, NULL, NULL);
		tbcat(buf, ".\n");
	    }
	}
    }
    if (!g_see_all()) {
    	tbcat(buf, "\nVision:\n");
	tbprintf(buf, "%d%% chance to be seen at outset of game.\n",
		 u_already_seen(u));
	tbprintf(buf, "%d%% chance to be seen at outset of game if independent.\n",
		 u_already_seen_indep(u));
	if (u_see_always(u))
	  tbcat(buf, "Always seen if terrain has been seen.\n");
	else
	  tbcat(buf, "Not always seen even if terrain has been seen.\n");
	/* (should only say if can be an occupant) */
	if (uu_table_row_not_default(u, uu_occ_vision, 100)) {
	    tbcat(buf, "Vision effect when occupying: ");
	    uu_table_row_desc(buf, u, uu_occ_vision, NULL, "in");
	    tbcat(buf, ".\n");
	}
	/* (should only say if unit can have occupants) */
	if (u_see_occupants(u))
	  tbcat(buf, "Occupants seen if unit has been seen.\n");
	else
	  tbcat(buf, "Occupants not seen even if unit has been seen.\n");
	switch (u_vision_range(u)) {
	  case -1:
	    tbcat(buf, "Can never see other units.\n");
	    break;
	  case 0:
	    tbcat(buf, "Can see other units at own location.\n");
	    break;
	  case 1:
	    /* Default range, no need to say anything. */
	    break;
	  default:
	    tbprintf(buf, "Can see units up to %d cells away.\n", u_vision_range(u));
	    break;
	}
	if (u_vision_range(u) > 0 && u_vision_bend(u) < 100) {
	    tbcat(buf, "Vision is line-of-sight");
	    if (u_vision_bend(u) > 0)
	      tbprintf(buf, "bending by %d%%", u_vision_bend(u));
	    tbcat(buf, ".\n");
	    if (ut_table_row_not_default(u, ut_eye_height, 0)) {
		tbcat(buf, "Effective eye height is ");
		ut_table_row_desc(buf, u, ut_eye_height, NULL, "in");
		tbcat(buf, ".\n");
	    }
	}
	if (ut_table_row_not_default(u, ut_body_height, 0)) {
	    tbcat(buf, "Effective body height is ");
	    ut_table_row_desc(buf, u, ut_body_height, NULL, "in");
	    tbcat(buf, ".\n");
	}
	if (ut_table_row_not_default(u, ut_weapon_height, 0)) {
	    tbcat(buf, "Effective weapon height is ");
	    ut_table_row_desc(buf, u, ut_weapon_height, NULL, "in");
	    tbcat(buf, ".\n");
	}
	if (u_vision_range(u) >= 0
	    && uu_table_row_not_default(u, uu_see_at, 100)) {
	    tbcat(buf, "Chance to see if in same cell is ");
	    uu_table_row_desc(buf, u, uu_see_at, tb_percent_desc, NULL);
	    tbcat(buf, ".\n");
	}
	if (u_vision_range(u) >= 1
	    && uu_table_row_not_default(u, uu_see_adj, 100)) {
	    tbcat(buf, "Chance to see if adjacent is ");
	    uu_table_row_desc(buf, u, uu_see_adj, tb_percent_desc, NULL);
	    tbcat(buf, ".\n");
	}
	if (u_vision_range(u) >= 2
	    && uu_table_row_not_default(u, uu_see, 100)) {
	    tbcat(buf, "Chance to see in general is ");
	    uu_table_row_desc(buf, u, uu_see, tb_percent_desc, NULL);
	    tbcat(buf, ".\n");
	}
	if (u_see_terrain_captured(u) > 0)
	  tbprintf(buf, "%d%% chance for enemy to see your terrain view if this type captured.\n",
		   u_see_terrain_captured(u));
    }
    if (ut_table_row_not_default(u, ut_accident_hit, 0)
	|| ut_table_row_not_default(u, ut_accident_vanish, 0)) {
	tbcat(buf, "\nAccidents:\n");
	if (ut_table_row_not_default(u, ut_accident_hit, 0)) {
	    tbcat(buf, "Chance to be damaged in an accident: ");
	    ut_table_row_desc(buf, u, ut_accident_hit, tb_percent_100th_desc, "in");
	    tbcat(buf, ".\n");
	    tbcat(buf, "Amount of damage: ");
	    ut_table_row_desc(buf, u, ut_accident_damage, tb_dice_desc, "in");
	    tbcat(buf, ".\n");
	}
	if (ut_table_row_not_default(u, ut_accident_vanish, 0)) {
	    tbcat(buf, "Chance to vanish in an accident: ");
	    ut_table_row_desc(buf, u, ut_accident_vanish,
			      tb_percent_100th_desc, "in");
	    tbcat(buf, ".\n");
	}
    }
    if (ut_table_row_not_default(u, ut_attrition, 0)) {
	tbcat(buf, "\nAttrition:\n");
	ut_table_row_desc(buf, u, ut_attrition, tb_percent_100th_desc, "in");
	tbcat(buf, ".\n");
    }
    /* Don't bother with this if economy code is not running. */
    if (nummtypes > 0 && g_economy()) {
	tbcat(buf, "\nMaterial Handling:\n");
	for_all_material_types(m) {
	    usesm = FALSE;
	    tbprintf(buf, "  %s", m_type_name(m));
	    if (um_base_production(u, m) > 0) {
		tbprintf(buf, ", %d basic production",
			 um_base_production(u, m));
		usesm = TRUE;
	    }
	    if (um_occ_production(u, m) >= 0) {
		tbprintf(buf, ", %d basic production if occupant",
			 um_occ_production(u, m));
		usesm = TRUE;
	    }
	    if (um_acp_to_extract(u, m) > 0) {
		tbprintf(buf, ", %d ACP to extract", um_acp_to_extract(u, m));
		usesm = TRUE;
	    }
	    if (um_storage_x(u, m) > 0) {
		tbprintf(buf, ", %d storage", um_storage_x(u, m));
		if (um_initial(u, m) > 0) {
		    tbprintf(buf, " (%d at start of game)",
			     min(um_initial(u, m), um_storage_x(u, m)));
		}
		usesm = TRUE;
	    }
	    if (um_base_consumption(u, m) > 0) {
		tbprintf(buf, ", %d basic consumption",
			 um_base_consumption(u, m));
		if (um_consumption_as_occupant(u, m) != 100) {
		    tbprintf(buf, ", times %d%% if occupant",
			     um_consumption_as_occupant(u, m));
		}
		usesm = TRUE;
	    }
	    if (um_to_act(u, m) != 0) {
		tbprintf(buf, ", needs %d to act at all",
			 um_to_act(u, m));
		usesm = TRUE;
	    }
	    if (um_to_move(u, m) != 0) {
		tbprintf(buf, ", needs %d to move",
			 um_to_move(u, m));
		usesm = TRUE;
	    }
	    if (um_consumption_per_move(u, m) != 0) {
		tbprintf(buf, ", %d consumed per move",
			 um_consumption_per_move(u, m));
		usesm = TRUE;
	    }
	    if (um_to_attack(u, m) != 0) {
		tbprintf(buf, ", needs %d to attack",
			 um_to_attack(u, m));
		usesm = TRUE;
	    }
	    if (um_consumption_per_attack(u, m) != 0) {
		tbprintf(buf, ", %d consumed per attack",
			 um_consumption_per_attack(u, m));
		usesm = TRUE;
	    }
	    if (u_acp_to_fire(u) > 0 && um_to_fire(u, m) != 0) {
		tbprintf(buf, ", needs %d to fire",
			 um_to_fire(u, m));
		usesm = TRUE;
	    }
	    if (um_to_create(u, m) != 0) {
		tbprintf(buf, ", builder needs %d to create",
			 um_to_create(u, m));
		usesm = TRUE;
	    }
	    if (um_consumption_on_creation(u, m) != 0) {
		tbprintf(buf, ", %d used in creation",
			 um_consumption_on_creation(u, m));
		usesm = TRUE;
	    }
	    if (um_to_build(u, m) != 0) {
		tbprintf(buf, ", builder needs %d to build",
			 um_to_build(u, m));
		usesm = TRUE;
	    }
	    if (um_consumption_per_build(u, m) != 0) {
		tbprintf(buf, ", %d used to add 1 CP",
			 um_consumption_per_build(u, m));
		usesm = TRUE;
	    }
	    if (um_to_repair(u, m) != 0) {
		tbprintf(buf, ", repairer needs %d to repair",
			 um_to_repair(u, m));
		usesm = TRUE;
	    }
	    if (um_consumption_per_repair(u, m) != 0) {
		tbprintf(buf, ", %d used to restore 1 HP",
			 um_consumption_per_repair(u, m));
		usesm = TRUE;
	    }
	    if (usesm) {
		if (um_inlength(u, m) > 0) {
		    tbprintf(buf, ", receive from up to %d cells away",
			     um_inlength(u, m));
		}
		if (um_outlength(u, m) > 0) {
		    tbprintf(buf, ", send up to %d cells away",
			     um_outlength(u, m));
		}
	    } else {
		tbcat(buf, " (none)");
	    }
	    tbcat(buf, "\n");
	}
	/* Productivity adjustment due to terrain applies to all
	   material types equally, but only display if unit
	   produces. */
	if (um_table_row_not_default(u, um_base_production, 0)) {
	    if (ut_table_row_not_default(u, ut_productivity, 100)) {
		tbcat(buf, "Productivity adjustment for terrain is ");
		ut_table_row_desc(buf, u, ut_productivity, tb_mult_desc, "in");
		tbcat(buf, ".\n");
	    }
	    if (ut_table_row_not_default(u, ut_productivity_adj, 0)) {
		tbcat(buf, "Productivity adjustment for adjacent terrain is ");
		ut_table_row_desc(buf, u, ut_productivity_adj, tb_mult_desc,
				  "in");
		tbcat(buf, ".\n");
	    }
	    if (um_table_row_not_default(u, um_productivity_min, 0)) {
		tbcat(buf, "Minimum net adjustment is ");
		um_table_row_desc(buf, u, um_productivity_min, tb_mult_desc);
		tbcat(buf, ".\n");
	    }
	    if (um_table_row_not_default(u, um_productivity_max, TABHI)) {
		tbcat(buf, "Maximum net adjustment is ");
		um_table_row_desc(buf, u, um_productivity_max, tb_mult_desc);
		tbcat(buf, ".\n");
	    }
	}
    }
    if (numatypes > 0) {
	/* Now also prints "None" if no advances are required. */
	int	found = FALSE;

	tbcat(buf, "\nRequired advances to build: ");
	for_all_advance_types(a) {
	    if (ua_needed_to_build(u, a) > 0) {
		if (found)
			tbprintf(buf, ", ");
		tbprintf(buf, "%s", a_type_name(a));
		found = TRUE;
	    }
	}
	if (found)
		tbprintf(buf, ".\n");		
	else	tbprintf(buf, "None.\n");
    }
    tbcat(buf, "\n");
    /* (should display weather interaction here) */
    if (uu_table_row_not_default(u, uu_auto_repair, 0)) {
	tbcat(buf, "Auto-repair of lost HP per turn: ");
	uu_table_row_desc(buf, u, uu_auto_repair, NULL, NULL);
	tbcat(buf, ".\n");
	if (uu_table_row_not_default(u, uu_auto_repair_range, 0)) {
	    tbcat(buf, "Range of auto-repair: ");
	    uu_table_row_desc(buf, u, uu_auto_repair_range, NULL, NULL);
	    tbcat(buf, ".\n");
	}
    }
    if (u_spy_chance(u) > 0 /* and random event in use */) {
	tbprintf(buf, "%d%% chance to spy, on units up to %d away.",
		 u_spy_chance(u), u_spy_range(u));
    }
    if (u_revolt(u) > 0 /* and random event in use */) {
	tb_fraction_desc(buf, u_revolt(u));
	tbcat(buf, "%% chance of revolt.\n");
    }
    if (u_lost_wreck(u) > 0
	|| u_lost_vanish(u) > 0
	|| u_lost_revolt(u) > 0
	|| uu_table_row_not_default(u, uu_lost_surrender, 0)) {
	tbcat(buf, "\nFate if side loses:");
	if (u_lost_wreck(u) > 0) {
	    tbcat(buf, "  ");
	    tb_percent_100th_desc(buf, u_lost_wreck(u));
	    tbcat(buf, " chance to wreck");
	}
	if (u_lost_wreck(u) < 10000 && u_lost_vanish(u) > 0) {
	    tbcat(buf, "  ");
	    tb_percent_100th_desc(buf, u_lost_vanish(u));
	    tbcat(buf, " chance to vanish");
	}
	if (u_lost_wreck(u) < 10000
	    && u_lost_vanish(u) < 10000
	    && u_lost_revolt(u) > 0) {
	    tbcat(buf, "  ");
	    tb_percent_100th_desc(buf, u_lost_revolt(u));
	    tbcat(buf, " chance to revolt");
	}
	if (u_lost_wreck(u) < 10000
	    && u_lost_vanish(u) < 10000
	    && u_lost_revolt(u) < 10000
	    && uu_table_row_not_default(u, uu_lost_surrender, 0)) {
	    tbcat(buf, "  chance to surrender to nearby unit, ");
	    uu_table_row_desc(buf, u, uu_lost_surrender,
			      tb_percent_100th_desc, "to");
	}
	if (u_lost_wreck(u) == 0
	    && u_lost_vanish(u) == 0
	    && u_lost_revolt(u) == 0
	    && !uu_table_row_not_default(u, uu_lost_surrender, 0))
	  tbcat(buf, " survival");
	tbcat(buf, ".\n");
    }
#ifdef DESIGNERS
    if (numdesigners > 0) {
	tbcat(buf, "FOR DESIGNERS:\n");
	tbprintf(buf, "Internal name is \"%s\".\n", u_internal_name(u));
	tbprintf(buf, "Short name is \"%s\".\n", u_short_name(u));
	if (u_assign_number(u))
	  tbcat(buf, "New units get assigned a number.\n");
    }
#endif /* DESIGNERS */
}

int
may_detonate(int u)
{
    return ((u_acp(u) > 0 && u_acp_to_detonate(u) > 0)
	    || u_detonate_on_death(u) > 0
	    || uu_table_row_not_default(u, uu_detonate_on_hit, 0)
	    || uu_table_row_not_default(u, uu_detonate_on_capture, 0)
	    || uu_table_row_not_default(u, uu_detonate_approach_range, -1)
	    || ut_table_row_not_default(u, ut_detonation_accident, 0)
	    );
}

static void
describe_mtype(int m, char *key, TextBuffer *buf)
{
    append_help_phrase(buf, m_help(m));
    /* Display the designer's notes for this type. */
    if (m_notes(m) != lispnil) {
	tbcat(buf, "\nNotes:\n");
	append_notes(buf, m_notes(m));
	tbcat(buf, "\n\n");
    }
    if (m_people(m) > 0) {
	tbprintf(buf, "1 of this represents %d individuals.", m_people(m));
    }
    /* (should display unit columns here) */
    /* Add an extra space, to make sure the description isn't empty. */
    tbcat(buf, " ");
}

static void
describe_ttype(int t, char *key, TextBuffer *buf)
{
    int m, ct;

    append_help_phrase(buf, t_help(t));
    /* Display the subtype. */
    switch (t_subtype(t)) {
      case cellsubtype:
	break;
      case bordersubtype:
	tbcat(buf, " (a border type)\n");
	break;
      case connectionsubtype:
	tbcat(buf, " (a connection type)\n");
	break;
      case coatingsubtype:
	tbcat(buf, " (a coating type)\n");
	break;
    }
    /* Display the designer's notes for this type. */
    if (t_notes(t) != lispnil) {
	tbcat(buf, "\nNotes:\n");
	append_notes(buf, t_notes(t));
	tbcat(buf, "\n\n");
    }
    if (t_liquid(t))
      tbcat(buf, "Represents water or other liquid.\n");
    tbprintf(buf, "Generic unit capacity is %d.\n", t_capacity(t));
    if (t_people_max(t) >= 0) {
	tbprintf(buf, "Up to %d people may live in this type of terrain.\n",
		 t_people_max(t));
    }
    if (any_elev_variation) {
	if (t_elev_min(t) == t_elev_max(t)) {
	    tbprintf(buf, "Elevation is always %d.\n",
		    t_elev_min(t));
	} else {
	    tbprintf(buf, "Elevations fall between %d and %d.\n",
		    t_elev_min(t), t_elev_max(t));
	}
    }
    if (t_thickness(t) > 0) {
	tbprintf(buf, "Thickness is %d.\n", t_thickness(t));
    }
    if (any_temp_variation) {
	if (t_temp_min(t) == t_temp_max(t)) {
	    tbprintf(buf, "Temperature is always %d.\n",
		     t_temp_min(t));
	} else {
	    tbprintf(buf, "Temperatures fall between %d and %d, averaging %d.\n",
		     t_temp_min(t), t_temp_max(t), t_temp_avg(t));
	}
	if (t_temp_variability(t) > 0) {
	    tbprintf(buf, "Temperature varies randomly, up to %d each turn.\n",
		     t_temp_variability(t));
	}
    }
    if (any_wind_variation) {
	if (t_wind_force_min(t) == t_wind_force_max(t)) {
	    tbprintf(buf, "Wind force is always %d.\n",
		     t_wind_force_min(t));
	} else {
	    tbprintf(buf, "Wind forces fall between %d and %d, averaging %d.\n",
		     t_wind_force_min(t), t_wind_force_max(t), t_wind_force_avg(t));
	}
	if (t_wind_force_variability(t) > 0) {
	    tbprintf(buf, "%d%% chance each turn that wind force will change.\n",
		     t_wind_force_variability(t));
	}
	if (t_wind_variability(t) > 0) {
	    tbprintf(buf, "%d%% chance each turn that wind direction will change.\n",
		     t_wind_variability(t));
	}
    }
    if (any_clouds) {
	if (t_clouds_min(t) == t_clouds_max(t)) {
	    tbprintf(buf, "Cloud cover is always %d.\n",
		     t_clouds_min(t));
	} else {
	    tbprintf(buf, "Cloud cover falls between %d and %d\n",
		     t_clouds_min(t), t_clouds_max(t));
	}
    }
    /* Display relationships with materials. */
    if (nummtypes > 0) {
	for_all_material_types(m) {
	    if (tm_storage_x(t, m) > 0
		|| tm_production(t, m) > 0
		|| tm_consumption(t, m) > 0) {
		tbprintf(buf, "%s:", m_type_name(m));
	    }
	    if (tm_storage_x(t, m) > 0) {
	    	tbprintf(buf, "  Can store up to %d", tm_storage_x(t, m));
	    	tbprintf(buf, " (normally starts game with %d)",
			 min(tm_initial(t, m), tm_storage_x(t, m)));
	    	tbcat(buf, ".\n");
	    	tbprintf(buf, "  Sides will%s always know current amount accurately.\n",
			 (tm_see_always(t, m) ? "" : " not"));
	    }
	    if (tm_production(t, m) > 0 || tm_consumption(t, m) > 0) {
		tbprintf(buf, "  Produces %d and consumes %d each turn.\n",
			 tm_production(t, m), tm_consumption(t, m));
	    }
	}
    }
    /* Display relationships with any coating terrain types. */
    if (numcoattypes > 0) {
	tbcat(buf, "Coatings:\n");
    	for_all_terrain_types(ct) {
	    if (t_is_coating(ct)) {
		tbprintf(buf, "%s coats, depths %d up to %d",
			 t_type_name(ct),
			 tt_coat_min(ct, t), tt_coat_max(ct, t));
	    }
    	}
    }
    /* Display damaged types. */
    if (tt_table_row_not_default(t, tt_damaged_type, 0)) {
	tbcat(buf, "  Type after being damaged: ");
	tt_table_row_desc(buf, t, tt_damaged_type, NULL);
	tbcat(buf, ".\n");
    }
    /* Display exhaustion types. */
    if (nummtypes > 0) {
	for_all_material_types(m) {
	    if (tm_change_on_exhaust(t, m) > 0 && tm_exhaust_type(t, m) != NONTTYPE) {
		tbprintf(buf, "If exhausted of %s, %d%% chance to change to %s.\n",
			 m_type_name(m),
			 tm_change_on_exhaust(t, m),
			 t_type_name(tm_exhaust_type(t, m))
			 );
	    }
	}
    }
#ifdef DESIGNERS
    if (numdesigners > 0) {
	tbcat(buf, "\nFOR DESIGNERS:\n");
	if (t_subtype_x(t) == c_number(symbol_value(intern_symbol(keyword_name(K_RIVER_X))))) {
	    tbcat(buf, "Considered to be a type of river.\n");
	} else if (t_subtype_x(t) == c_number(symbol_value(intern_symbol(keyword_name(K_ROAD_X))))) {
	    tbcat(buf, "Considered to be a type of road.\n");
	}
	if (tt_table_row_not_default(t, tt_drawable, 1)) {
	    tbcat(buf, "Draw over terrain: ");
	    tt_table_row_desc(buf, t, tt_drawable, NULL);
	    tbcat(buf, ".\n");
	}
    }
#endif /* DESIGNERS */
}

static void
describe_atype(int a, char *key, TextBuffer *buf)
{
    append_help_phrase(buf, a_help(a));
    /* Display the designer's notes for this type. */
    if (a_notes(a) != lispnil) {
	tbcat(buf, "\nNotes:\n");
	append_notes(buf, a_notes(a));
	tbcat(buf, "\n\n");
    }
    tbprintf(buf, "Research points (RP) to discover: %d.\n", a_rp(a));
    tbcat(buf, "\n");
    if (aa_table_row_not_default(a, aa_needed_to_research, 0)) {
	tbcat(buf, "Prerequisite advances: ");
	aa_table_row_desc(buf, a, aa_needed_to_research, tb_bool_desc);
	tbcat(buf, ".\n");
    } else {
	tbcat(buf, "No prerequisites.\n");
    }
    if (aa_table_column_not_default(a, aa_needed_to_research, 0)) {
	tbcat(buf, "Enables: ");
	aa_table_column_desc(buf, a, aa_needed_to_research, tb_bool_desc);
	tbcat(buf, ".\n");
    } else {
	tbcat(buf, "Enables no further advances.\n");
    }
}

static void
describe_scorekeepers(int arg, char *key, TextBuffer *buf)
{
    int i = 1;
    Scorekeeper *sk;

    if (scorekeepers == NULL) {
	tbcat(buf, "No scores are being kept.");
    } else {
	for_all_scorekeepers(sk) {
	    if (numscorekeepers > 1) {
		tbprintf(buf, "%d.  ", i++);
	    }
	    if (symbolp(sk->body)
		&& match_keyword(sk->body, K_LAST_SIDE_WINS)) {
		tbcat(buf, "The last side left in the game wins.");
		/* (should mention point values also) */
	    } else if (symbolp(sk->body)
		&& match_keyword(sk->body, K_LAST_ALLIANCE_WINS)) {
		tbcat(buf, "The last alliance left in the game wins.");
		/* (should mention point values also) */
	    } else {
		tbcat(buf, "(an indescribably complicated scorekeeper)");
	    }
	    tbcat(buf, "\n");
	}
    }
}

/* List each synthesis method and its parameters. */

static void
describe_setup(int arg, char *key, TextBuffer *buf)
{
    int u, t, t2, methkey;
    Obj *synthlist, *methods, *method;
    
    
    synthlist = g_synth_methods();
    if (synthlist == lispnil)
      tbcat(buf, "No synthesis done when setting up this game.\n");
    else
      tbcat(buf, "Synthesis done when setting up this game:\n");
    for (methods = synthlist; methods != lispnil; methods = cdr(methods)) {
	method = car(methods);
	if (symbolp(method)) {
	    methkey = keyword_code(c_string(method));
	    switch (methkey) {
	      case K_MAKE_COUNTRIES:
		tbcat(buf, "\nCountries:");
		describe_synth_run(buf, methkey);
		tbcat(buf, "\n");
		tbprintf(buf, "  %d cells across, between %d and %d cells apart.\n",
			2 * g_radius_min() + 1,
			g_separation_min(), g_separation_max());
		if (t_property_not_default(t_country_min, 0)) {
		    tbcat(buf, "  Minimum terrain in each country: ");
		    t_property_desc(buf, t_country_min, NULL);
		    tbcat(buf, ".\n");
		}
		if (t_property_not_default(t_country_max, -1)) {
		    tbcat(buf, "  Maximum terrain in each country: ");
		    t_property_desc(buf, t_country_max, NULL);
		    tbcat(buf, ".\n");
		}
		if (u_property_not_default(u_start_with, 0)) {
		    tbcat(buf, "  Start with: ");
		    u_property_desc(buf, u_start_with, NULL);
		    tbcat(buf, ".\n");
		}
		if (u_property_not_default(u_indep_near_start, 0)) {
		    tbcat(buf, "  Independents nearby: ");
		    u_property_desc(buf, u_indep_near_start, NULL);
		    tbcat(buf, ".\n");
		}
		tbcat(buf, "  Favored terrain:\n");
		for_all_unit_types(u) {
		    if (u_start_with(u) > 0 || u_indep_near_start(u)) {
			tbprintf(buf, "  %s: ", u_type_name(u));
			ut_table_row_desc(buf, u, ut_favored, NULL, NULL);
			tbcat(buf, "\n");
		    }
		}
		if (g_radius_max() != 0) {
		    tbcat(buf, "Country growth:\n");
		    if (g_radius_max() == -1) {
			tbcat(buf, "  Up to entire world");
		    } else {
			tbprintf(buf, "  Up to %d cells across",
				 2 * g_radius_max() + 1);
		    }
		    tbprintf(buf, ", %d chance to stop if blocked.\n",
			     g_growth_stop());
		    if (t_property_not_default(t_country_growth, 100)) {
			tbcat(buf, "  Growth chance, by terrain: ");
			t_property_desc(buf, t_country_max, tb_percent_desc);
			tbcat(buf, ".\n");
		    }
		    if (t_property_not_default(t_country_takeover, 0)) {
			tbcat(buf, "  Takeover chance, by terrain: ");
			t_property_desc(buf, t_country_takeover, NULL);
			tbcat(buf, ".\n");
		    }
		    if (u_property_not_default(u_unit_growth, 0)) {
			tbcat(buf, "  Chance for additional unit: ");
			u_property_desc(buf, u_unit_growth, NULL);
			tbcat(buf, ".\n");
		    }
		    if (u_property_not_default(u_indep_growth, 0)) {
			tbcat(buf, "  Chance for additional independent unit: ");
			u_property_desc(buf, u_indep_growth, NULL);
			tbcat(buf, ".\n");
		    }
		    if (u_property_not_default(u_unit_takeover, 0)) {
			tbcat(buf, "  Chance to take over units: ");
			u_property_desc(buf, u_unit_takeover, NULL);
			tbcat(buf, ".\n");
		    }
		    if (u_property_not_default(u_indep_takeover, 0)) {
			tbcat(buf, "  Chance to take over independent unit: ");
			u_property_desc(buf, u_indep_takeover, NULL);
			tbcat(buf, ".\n");
		    }
		    if (u_property_not_default(u_country_units_max, -1)) {
			tbcat(buf, "  Maximum units in country: ");
			u_property_desc(buf, u_country_units_max, NULL);
			tbcat(buf, ".\n");
		    }
		    if (t_property_not_default(t_country_people, 0)) {
			tbcat(buf, "  People takeover chance, by terrain: ");
			t_property_desc(buf, t_country_people, NULL);
			tbcat(buf, ".\n");
		    }
		    if (t_property_not_default(t_indep_people, 0)) {
			tbcat(buf, "  Independent people chance: ");
			t_property_desc(buf, t_indep_people, NULL);
			tbcat(buf, ".\n");
		    }
		}
		break;
	      case K_MAKE_EARTHLIKE_TERRAIN:
		tbcat(buf, "\nEarthlike terrain:");
		describe_synth_run(buf, methkey);
		tbcat(buf, "\n");
		tbprintf(buf, "  Terrain around edge is %s.\n",
			 t_type_name(g_edge_terrain()));
		/* (should describe tt_adj_terr_effect workings here) */
		break;
	      case K_MAKE_FRACTAL_PTILE_TERRAIN:
		tbcat(buf, "\nFractal percentile terrain:");
		describe_synth_run(buf, methkey);
		tbcat(buf, "\n");
		tbprintf(buf, "  Alt blobs density %d, size %d, height %d\n",
			g_alt_blob_density(), g_alt_blob_size(),
			 g_alt_blob_height());
		tbprintf(buf, "    %d smoothing passes\n", g_alt_smoothing());
		tbcat(buf, "    Lower percentiles: ");
		t_property_desc(buf, t_alt_min, NULL);
		tbcat(buf, ".\n");
		tbcat(buf, "    Upper percentiles: ");
		t_property_desc(buf, t_alt_max, NULL);
		tbcat(buf, ".\n");
		tbprintf(buf, "  Wet blobs density %d, size %d, height %d\n",
			g_wet_blob_density(), g_wet_blob_size(),
			 g_wet_blob_height());
		tbprintf(buf, "    %d smoothing passes\n", g_wet_smoothing());
		tbcat(buf, "    Lower percentiles: ");
		t_property_desc(buf, t_wet_min, NULL);
		tbcat(buf, ".\n");
		tbcat(buf, "    Upper percentiles: ");
		t_property_desc(buf, t_wet_max, NULL);
		tbcat(buf, ".\n");
		tbprintf(buf, "  Terrain around edge is %s.\n",
			 t_type_name(g_edge_terrain()));
		/* (should describe tt_adj_terr_effect workings here) */
		break;
	      case K_MAKE_INDEPENDENT_UNITS:
		tbcat(buf, "\nIndependent units:");
		describe_synth_run(buf, methkey);
		tbcat(buf, "\n");
		for_all_unit_types(u) {
		    if (ut_table_row_not_default(u, ut_indep_density, 0)) {
			tbprintf(buf, "  Chance of independent %s:",
				 u_type_name(u));
			ut_table_row_desc(buf, u, ut_indep_density,
					  tb_percent_100th_desc, "in");
			tbcat(buf, ".\n");
		    }
		}
		/* (should show indep people) */
		break;
	      case K_MAKE_INITIAL_MATERIALS:
		tbcat(buf, "\nMaterials:");
		describe_synth_run(buf, methkey);
		tbcat(buf, "\n");
		/* (should show unit and terrain initial supply) */
		break;
	      case K_MAKE_MAZE_TERRAIN:
		tbcat(buf, "\nMaze terrain:");
		describe_synth_run(buf, methkey);
		tbcat(buf, "\n");
		tbprintf(buf, "  %d.%2.2d%% of maze is room.\n",
			 g_maze_room() / 100, g_maze_room() % 100);
		tbcat(buf, "Room terrain types will be");
		for_all_terrain_types(t) {
		    if (t_maze_room_occurrence(t) > 0) {
			tbprintf(buf, " %s(%d)", t_type_name(t),
				 t_maze_room_occurrence(t));
		    }
		}
		tbcat(buf, ".\n");
		tbprintf(buf, "  %d.%2.2d%% of maze is passageway.\n",
			 g_maze_passage() / 100, g_maze_passage() % 100);
		tbcat(buf, "Passageway terrain types will be");
		for_all_terrain_types(t) {
		    if (t_maze_passage_occurrence(t) > 0) {
			tbprintf(buf, " %s(%d)", t_type_name(t),
				 t_maze_passage_occurrence(t));
		    }
		}
		tbcat(buf, ".\n");
		tbprintf(buf, "  Terrain around edge is %s.\n",
			 t_type_name(g_edge_terrain()));
		break;
	      case K_MAKE_RANDOM_DATE:
		tbcat(buf, "\nRandom date:");
		describe_synth_run(buf, methkey);
		tbcat(buf, "\n");
		break;
	      case K_MAKE_RANDOM_TERRAIN:
		tbcat(buf, "\nRandom terrain:");
		describe_synth_run(buf, methkey);
		tbcat(buf, "\n");
		tbcat(buf, "  Terrain types will be");
		for_all_terrain_types(t) {
		    if (t_occurrence(t) > 0) {
			tbprintf(buf, " %s(%d)", t_type_name(t),
				 t_occurrence(t));
		    }
		}
		tbcat(buf, ".\n");
		tbprintf(buf, "  Terrain around edge is %s.\n",
			 t_type_name(g_edge_terrain()));
		break;
	      case K_MAKE_RIVERS:
		tbcat(buf, "\nRivers:");
		describe_synth_run(buf, methkey);
		tbcat(buf, "\n");
		if (t_property_not_default(t_river_chance, 0)) {
		    tbcat(buf, "  Chance to be river source: ");
		    t_property_desc(buf, t_river_chance,
				    tb_percent_100th_desc);
		    tbcat(buf, ".\n");
		    if (g_river_sink_terrain() != NONTTYPE)
		      tbprintf(buf, "  Sink is %s.\n",
			       t_type_name(g_river_sink_terrain()));
		    else
		      tbcat(buf, "  No special sink terrain.\n");
		}
		for_all_terrain_types(t) {
		    if (tt_table_row_not_default(t, tt_adj_terr_effect, -1)) {
			tbprintf(buf, "  River of type %s incompatible with",
			      t_type_name(t));
			for_all_terrain_types(t2) {
			    if (tt_adj_terr_effect(t, t2) >= 0)
			      tbprintf(buf, " %s", t_type_name(t2));
			}
		    }
		}
		break;
	      case K_MAKE_ROADS:
		tbcat(buf, "\nRoads:");
		describe_synth_run(buf, methkey);
		tbcat(buf, "\n");
		tbcat(buf, "  Chance to run:\n");
		for_all_unit_types(u) {
		    if (uu_table_row_not_default(u, uu_road_chance, 0)) {
			tbprintf(buf, "  From %s: ", u_type_name(u));
			uu_table_row_desc(buf, u, uu_road_chance,
					  tb_percent_desc, "to");
			tbcat(buf, "\n");
		    }
		    if (u_spur_chance(u) > 0) {
			tbprintf(buf, "    %d%% chance of spur, if within %d of road.\n",
				 u_spur_chance(u), u_spur_range(u));
		    }
		    if (u_road_to_edge_chance(u) > 0) {
			tbprintf(buf, "    %d%% chance of road to edge.\n",
				 u_road_to_edge_chance(u));
		    }
		}
		for_all_terrain_types(t) {
		    if (t_subtype(t) == cellsubtype
			&& tt_table_row_not_default(t, tt_road_into_chance, 0)) {
			tbprintf(buf, "  Routing of road from %s: ",
				 t_type_name(t));
			/* Note: this is actually a weight, not a
			   percent chance. */
			tt_table_row_desc(buf, t, tt_road_into_chance,
					  tb_value_desc /*, "to" */);
			tbcat(buf, ".\n");
		    }
		}
		if (g_edge_road_density() > 0) {
		    tbcat(buf, "  ");
		    tb_percent_100th_desc(buf, g_edge_road_density());
		    tbcat(buf, "  of edge gets road run to another edge.\n");
		}
		break;
	      case K_MAKE_WEATHER:
		tbcat(buf, "\nWeather:");
		describe_synth_run(buf, methkey);
		tbcat(buf, "\n");
		break;
	      case K_NAME_GEOGRAPHICAL_FEATURES:
		tbcat(buf, "\nNames for geographical features:");
		describe_synth_run(buf, methkey);
		tbcat(buf, "\n");
		break;
	      case K_NAME_UNITS_RANDOMLY:
		tbcat(buf, "\nNames for units:");
		describe_synth_run(buf, methkey);
		tbcat(buf, "\n");
		break;
	      default:
		tbprintf(buf, "\n%s:", c_string(method));
		describe_synth_run(buf, methkey);
		tbcat(buf, "\n");
		break;
	    }
	} else if (consp(method)) {
	    /* (what?) */
	}
    }
    tbcat(buf, "\n");
    if (g_side_lib() != lispnil)
      tbprintf(buf, "%d choices in side library.\n", length(g_side_lib()));
}

static void
describe_synth_run(TextBuffer *buf, int methkey)
{
    int calls, runs;
  
    if (get_synth_method_uses(methkey, &calls, &runs)) {
	if (calls > 0) {
	    if (calls == 1 && runs == 1) {
		tbcat(buf, " (was run)");
	    } else if (calls == 1 && runs == 0) {
		tbcat(buf, " (was not run)");
	    } else {
		tbprintf(buf, " (was called %d times, was run %d times)",
			calls, runs);
	    }
	} else {
	    tbcat(buf, " (not attempted)");
	}
    } else {
	tbcat(buf, " (???)");
    }
}

static void
describe_world(int arg, char *key, TextBuffer *buf)
{
    tbprintf(buf, "World circumference: %d.\n", world.circumference);
    tbcat(buf, "\n");
    tbprintf(buf, "Area in world: %d wide x %d high", area.width, area.height);
    if (area.width == world.circumference)
      tbcat(buf, " (wraps completely around world).  ");
    else
      tbcat(buf, ".  ");

    tbprintf(buf, "Latitude: %d.  Longitude: %d.\n", area.latitude, area.longitude);
    tbcat(buf, "\n");
    if (elevations_defined()) {
	tbprintf(buf, "Elevations range from %d to %d, averaging %d\n",
		 area.minelev, area.maxelev, area.avgelev);
	tbprintf(buf, "Cells are %d across.\n", area.cellwidth);
    }
    if (world.yearlength > 1) {
	tbprintf(buf, "Length of year: %d turns.\n", world.yearlength);
    }
    if (world.daylength != 1) {
	tbprintf(buf, "Length of day: %d turns.\n", world.daylength);
	tbprintf(buf, "Percentage daylight: %d%%.\n", world.daylight_fraction);
	tbprintf(buf, "Percentage twilight: %d%%.\n",
		 world.twilight_fraction - world.daylight_fraction);
    }
    if (area.temp_year != lispnil) {
	/* (should describe temperature year cycle here) */
    }
#ifdef DESIGNERS
    if (numdesigners > 0) {
	tbcat(buf, "FOR DESIGNERS:\n");
	tbprintf(buf, "Area projection is %d.\n", area.projection);
	tbprintf(buf, "Default contour line color is \"%s\".\n",
		 g_contour_color());
	tbprintf(buf, "Default country border color is \"%s\".\n",
		 g_country_border_color());
	tbprintf(buf, "Default frontline color is \"%s\".\n",
		 g_frontline_color());
	tbprintf(buf, "Default grid color is \"%s\".\n",
		 g_grid_color());
	tbprintf(buf, "Default meridian color is \"%s\".\n",
		 g_meridian_color());
	tbprintf(buf, "Default shoreline color is \"%s\".\n",
		 g_shoreline_color());
	tbprintf(buf, "Default unit name color is \"%s\".\n",
		 g_unit_name_color());
	tbprintf(buf, "Default unseen color is \"%s\".\n",
		 g_unseen_color());
    }
#endif /* DESIGNERS */
}

/* The following globals don't make sense for online help, but are listed
   here so that a cross-check of *.def and online help doesn't list them
   as undocumented: g_random_state g_run_serial_number g_turn. */

/* This describes a command (from cmd.def et al) in a way that all
   interfaces can use. */

void
describe_command (int ch, char *name, char *help, int onechar, TextBuffer *buf)
{
    if (onechar && ch != '\0') {
	if (ch < ' ' || ch > '~') { 
	    tbprintf(buf, "^%c  ", (ch ^ 0x40));
	} else if (ch == ' ') {
	    tbprintf(buf, "'%c' ", ch);
	} else {
	    tbprintf(buf, " %c  ", ch);
	}
    } else if (!onechar && ch == '\0') {
	tbcat(buf, "\"");
	tbcat(buf, name);
	tbcat(buf, "\"");
    } else
      return;
    tbcat(buf, " ");
    tbcat(buf, help);
    tbcat(buf, "\n");
}

static int
u_property_not_default(int (*fn)(int i), int dflt)
{
    int u, val;

    for_all_unit_types(u) {
	val = (*fn)(u);
	if (val != dflt)
	  return TRUE;
    }
    return FALSE;
}

static int
t_property_not_default(int (*fn)(int i), int dflt)
{
    int t, val;

    for_all_terrain_types(t) {
	val = (*fn)(t);
	if (val != dflt)
	  return TRUE;
    }
    return FALSE;
}

static int
uu_table_row_not_default(u, fn, dflt)
int u, dflt;
int (*fn)(int i, int j);
{
    int u2, val2;

    for_all_unit_types(u2) {
	val2 = (*fn)(u, u2);
	if (val2 != dflt)
	  return TRUE;
    }
    return FALSE;
}

static int
ut_table_row_not_default(u, fn, dflt)
int u, dflt;
int (*fn)(int i, int j);
{
    int t, val2;

    for_all_terrain_types(t) {
	val2 = (*fn)(u, t);
	if (val2 != dflt)
	  return TRUE;
    }
    return FALSE;
}

static int
um_table_row_not_default(u, fn, dflt)
int u, dflt;
int (*fn)(int i, int j);
{
    int m, val2;

    for_all_material_types(m) {
	val2 = (*fn)(u, m);
	if (val2 != dflt)
	  return TRUE;
    }
    return FALSE;
}

static int
tt_table_row_not_default(t1, fn, dflt)
int t1, dflt;
int (*fn)(int i, int j);
{
    int t2, val2;

    for_all_terrain_types(t2) {
	val2 = (*fn)(t1, t2);
	if (val2 != dflt)
	  return TRUE;
    }
    return FALSE;
}

#if 0 /* not used currently */
static int
tm_table_row_not_default(t, fn, dflt)
int t, dflt;
int (*fn)(int i, int j);
{
    int m, val2;

    for_all_material_types(m) {
	val2 = (*fn)(t, m);
	if (val2 != dflt)
	  return TRUE;
    }
    return FALSE;
}
#endif

static int
aa_table_row_not_default(a1, fn, dflt)
int a1, dflt;
int (*fn)(int i, int j);
{
    int a2, val2;

    for_all_advance_types(a2) {
	val2 = (*fn)(a1, a2);
	if (val2 != dflt)
	  return TRUE;
    }
    return FALSE;
}

static int
uu_table_column_not_default(u, fn, dflt)
int u, dflt;
int (*fn)(int i, int j);
{
    int u2, val2;

    for_all_unit_types(u2) {
	val2 = (*fn)(u2, u);
	if (val2 != dflt)
	  return TRUE;
    }
    return FALSE;
}

static int
aa_table_column_not_default(a1, fn, dflt)
int a1, dflt;
int (*fn)(int i, int j);
{
    int a2, val2;

    for_all_advance_types(a2) {
	val2 = (*fn)(a2, a1);
	if (val2 != dflt)
	  return TRUE;
    }
    return FALSE;
}

struct histo {
    int val, num;
};

/* This compare will sort histogram entries in *reverse* order
   (most common values first). */

static int
histogram_compare(CONST void *h1, CONST void *h2)
{
    if (((struct histo *) h2)->num != ((struct histo *) h1)->num) {
    	return ((struct histo *) h2)->num - ((struct histo *) h1)->num;
    } else {
    	return ((struct histo *) h2)->val - ((struct histo *) h1)->val;
    }
}

static struct histo *u_histogram;

static void
u_property_desc(TextBuffer *buf, int (*fn)(int i),
		void (*formatter)(TextBuffer *buf, int val))
{
    int val, u, val2, constant = TRUE, found;
    int i, numentries, first;

    if (formatter == NULL)
      formatter = tb_value_desc;
    if (u_histogram == NULL)
      u_histogram =
	(struct histo *) xmalloc(numutypes * sizeof(struct histo));
    /* Compute a histogram of all the values for the given property. */
    numentries = 0;
    val = (*fn)(0);
    u_histogram[numentries].val = val;
    u_histogram[numentries].num = 1;
    ++numentries;
    for_all_unit_types(u) {
	val2 = (*fn)(u);
	if (val2 == val) {
	    ++(u_histogram[0].num);
	} else {
	    constant = FALSE;
	    found = FALSE;
	    for (i = 1; i < numentries; ++i) {
		if (val2 == u_histogram[i].val) {
		    ++(u_histogram[i].num);
		    found = TRUE;
		    break;
		}
	    }
	    if (!found) {
		u_histogram[numentries].val = val2;
		u_histogram[numentries].num = 1;
		++numentries;
	    }
	}
    }
    /* The constant table/row case is easily disposed of. */
    if (constant) {
	(*formatter)(buf, val);
	tbcat(buf, " for all unit types");
    	return;
    }
    /* Not a constant row; sort the histogram and compose a description. */
    qsort(u_histogram, numentries, sizeof(struct histo), histogram_compare);
    /* Show a "by default" clause if at least half of the entries are all
       the same. */
    if (u_histogram[0].num * 2 >= numutypes) {
	(*formatter)(buf, u_histogram[0].val);
	tbcat(buf, " by default");
    	i = 1;
    } else {
    	i = 0;
    }
    for (; i < numentries; ++i) {
	if (i > 0)
	  tbcat(buf, ", ");
	(*formatter)(buf, u_histogram[i].val);
	tbcat(buf, " for ");
	first = TRUE;
	for_all_unit_types(u) {
	    if ((*fn)(u) == u_histogram[i].val) {
		if (!first)
		  /* For this and similar situations, we need a space
		     in addition to the comma, so interface elements
		     (such as the Mac's text display) can decide to
		     add line breaks. */
		  tbcat(buf, ", ");
		else
		  first = FALSE;
		tbcat(buf, u_type_name(u));
	    }
	}
    }
}

static struct histo *t_histogram;

static void
t_property_desc(TextBuffer *buf, int (*fn)(int i),
		void (*formatter)(TextBuffer *buf, int val))
{
    int val, t, val2, constant = TRUE, found;
    int i, numentries, first;

    if (formatter == NULL)
      formatter = tb_value_desc;
    if (t_histogram == NULL)
      t_histogram =
	(struct histo *) xmalloc(numttypes * sizeof(struct histo));
    /* Compute a histogram of all the values for the given property. */
    numentries = 0;
    val = (*fn)(0);
    t_histogram[numentries].val = val;
    t_histogram[numentries].num = 1;
    ++numentries;
    for_all_terrain_types(t) {
	val2 = (*fn)(t);
	if (val2 == val) {
	    ++(t_histogram[0].num);
	} else {
	    constant = FALSE;
	    found = FALSE;
	    for (i = 1; i < numentries; ++i) {
		if (val2 == t_histogram[i].val) {
		    ++(t_histogram[i].num);
		    found = TRUE;
		    break;
		}
	    }
	    if (!found) {
		t_histogram[numentries].val = val2;
		t_histogram[numentries].num = 1;
		++numentries;
	    }
	}
    }
    /* The constant table/row case is easily disposed of. */
    if (constant) {
	(*formatter)(buf, val);
	tbcat(buf, " for all terrain types");
    	return;
    }
    /* Not a constant row; sort the histogram and compose a description. */
    qsort(t_histogram, numentries, sizeof(struct histo), histogram_compare);
    /* Show a "by default" clause if at least half of the entries are all
       the same. */
    if (t_histogram[0].num * 2 >= numttypes) {
	(*formatter)(buf, t_histogram[0].val);
	tbcat(buf, " by default");
    	i = 1;
    } else {
    	i = 0;
    }
    for (; i < numentries; ++i) {
	if (i > 0)
	  tbcat(buf, ", ");
	(*formatter)(buf, t_histogram[i].val);
	tbcat(buf, " for ");
	first = TRUE;
	for_all_terrain_types(t) {
	    if ((*fn)(t) == t_histogram[i].val) {
		if (!first)
		  tbcat(buf, ", ");
		else
		  first = FALSE;
		tbcat(buf, t_type_name(t));
	    }
	}
    }
}

/* Generate a textual description of a single unit's interaction with all other
   unit types wrt a given table. */

static void
uu_table_row_desc(TextBuffer *buf, int u, int (*fn)(int i, int j),
		  void (*formatter)(TextBuffer *buf, int val), char *connect)
{
    uu_table_rowcol_desc(buf, u, fn, formatter, connect, 0);
}

static void
uu_table_column_desc(TextBuffer *buf, int u, int (*fn)(int i, int j),
		     void (*formatter)(TextBuffer *buf, int val),
		     char *connect)
{
    uu_table_rowcol_desc(buf, u, fn, formatter, connect, 1);
}

static void
uu_table_rowcol_desc(TextBuffer *buf, int u, int (*fn)(int i, int j),
		     void (*formatter)(TextBuffer *buf, int val),
		     char *connect, int rowcol)
{
    int val, val2, u2, constant = TRUE, found;
    int i, numentries, first;

    if (formatter == NULL)
      formatter = tb_value_desc;
    if (empty_string(connect))
      connect = "for";
    if (u_histogram == NULL)
      u_histogram =
	(struct histo *) xmalloc(numutypes * sizeof(struct histo));
    val = (rowcol ? (*fn)(0, u) : (*fn)(u, 0));
    /* Compute a histogram of all the values in the row of the table. */
    numentries = 0;
    u_histogram[numentries].val = val;
    u_histogram[numentries].num = 1;
    ++numentries;
    for_all_unit_types(u2) {
	val2 = (rowcol ? (*fn)(u2, u) : (*fn)(u, u2));
	if (val2 == val) {
	    ++(u_histogram[0].num);
	} else {
	    constant = FALSE;
	    found = FALSE;
	    for (i = 1; i < numentries; ++i) {
		if (val2 == u_histogram[i].val) {
		    ++(u_histogram[i].num);
		    found = TRUE;
		    break;
		}
	    }
	    if (!found) {
		u_histogram[numentries].val = val2;
		u_histogram[numentries].num = 1;
		++numentries;
	    }
	}
    }
    /* The constant table/row case is easily disposed of. */
    if (constant) {
	(*formatter)(buf, val);
	tbprintf(buf, " %s all unit types", connect);
    	return;
    }
    /* Not a constant row; sort the histogram and compose a description. */
    qsort(u_histogram, numentries, sizeof(struct histo), histogram_compare);
    /* Show a "by default" clause if at least half of the entries are all
       the same. */
    if (u_histogram[0].num * 2 >= numutypes) {
	(*formatter)(buf, u_histogram[0].val);
	tbcat(buf, " by default");
    	i = 1;
    } else {
    	i = 0;
    }
    for (; i < numentries; ++i) {
	if (i > 0)
	  tbcat(buf, ", ");
	(*formatter)(buf, u_histogram[i].val);
	tbprintf(buf, " %s ", connect);
	first = TRUE;
	for_all_unit_types(u2) {
	    val2 = (rowcol ? (*fn)(u2, u) : (*fn)(u, u2));
	    if (val2 == u_histogram[i].val) {
		if (!first)
		  tbcat(buf, ", ");
		else
		  first = FALSE;
		tbcat(buf, u_type_name(u2));
	    }
	}
    }
}

/* Generate a textual description of a single unit's interaction with all
   terrain types wrt a given table. */

static void
ut_table_row_desc(TextBuffer *buf, int u, int (*fn)(int i, int j),
		  void (*formatter)(TextBuffer *buf, int val), char *connect)
{
    int val = (*fn)(u, 0), val2, t, constant = TRUE, found;
    int i, numentries, first;

    if (formatter == NULL)
      formatter = tb_value_desc;
    if (empty_string(connect))
      connect = "for";
    if (u_histogram == NULL)
      u_histogram =
	(struct histo *) xmalloc(numutypes * sizeof(struct histo));
    /* Compute a histogram of all the values in the row of the table. */
    numentries = 0;
    u_histogram[numentries].val = val;
    u_histogram[numentries].num = 1;
    ++numentries;
    for_all_terrain_types(t) {
	val2 = (*fn)(u, t);
	if (val2 == val) {
	    ++(u_histogram[0].num);
	} else {
	    constant = FALSE;
	    found = FALSE;
	    for (i = 1; i < numentries; ++i) {
		if (val2 == u_histogram[i].val) {
		    ++(u_histogram[i].num);
		    found = TRUE;
		    break;
		}
	    }
	    if (!found) {
		u_histogram[numentries].val = val2;
		u_histogram[numentries].num = 1;
		++numentries;
	    }
	}
    }
    /* The constant table/row case is easily disposed of. */
    if (constant) {
	(*formatter)(buf, val);
	tbprintf(buf, " %s all terrain types", connect);
    	return;
    }
    /* Not a constant row; sort the histogram and compose a description. */
    qsort(u_histogram, numentries, sizeof(struct histo), histogram_compare);
    /* Show a "by default" clause if at least half of the entries are all
       the same. */
    if (u_histogram[0].num * 2 >= numttypes) {
	(*formatter)(buf, u_histogram[0].val);
	tbcat(buf, " by default");
    	i = 1;
    } else {
    	i = 0;
    }
    for (; i < numentries; ++i) {
	if (i > 0)
	  tbcat(buf, ", ");
	(*formatter)(buf, u_histogram[i].val);
	tbprintf(buf, " %s ", connect);
	first = TRUE;
	for_all_terrain_types(t) {
	    if ((*fn)(u, t) == u_histogram[i].val) {
		if (!first)
		  tbcat(buf, ", ");
		else
		  first = FALSE;
		tbcat(buf, t_type_name(t));
	    }
	}
    }
}

static void
um_table_row_desc(TextBuffer *buf, int u, int (*fn)(int i, int j),
		  void (*formatter)(TextBuffer *buf, int val))
{
    int val = (*fn)(u, 0), val2, m, constant = TRUE, found;
    int i, numentries, first;
    char *connect = "vs";

    if (formatter == NULL)
      formatter = tb_value_desc;
    if (u_histogram == NULL)
      u_histogram =
	(struct histo *) xmalloc(numutypes * sizeof(struct histo));
    /* Compute a histogram of all the values in the row of the table. */
    numentries = 0;
    u_histogram[numentries].val = val;
    u_histogram[numentries].num = 1;
    ++numentries;
    for_all_material_types(m) {
	val2 = (*fn)(u, m);
	if (val2 == val) {
	    ++(u_histogram[0].num);
	} else {
	    constant = FALSE;
	    found = FALSE;
	    for (i = 1; i < numentries; ++i) {
		if (val2 == u_histogram[i].val) {
		    ++(u_histogram[i].num);
		    found = TRUE;
		    break;
		}
	    }
	    if (!found) {
		u_histogram[numentries].val = val2;
		u_histogram[numentries].num = 1;
		++numentries;
	    }
	}
    }
    /* The constant table/row case is easily disposed of. */
    if (constant) {
	(*formatter)(buf, val);
	tbprintf(buf, " %s all material types", connect);
    	return;
    }
    /* Not a constant row; sort the histogram and compose a description. */
    qsort(u_histogram, numentries, sizeof(struct histo), histogram_compare);
    /* Show a "by default" clause if at least half of the entries are all
       the same. */
    if (u_histogram[0].num * 2 >= nummtypes) {
	(*formatter)(buf, u_histogram[0].val);
	tbcat(buf, " by default");
    	i = 1;
    } else {
    	i = 0;
    }
    for (; i < numentries; ++i) {
	if (i > 0)
	  tbcat(buf, ", ");
	(*formatter)(buf, u_histogram[i].val);
	tbprintf(buf, " %s ", connect);
	first = TRUE;
	for_all_material_types(m) {
	    if ((*fn)(u, m) == u_histogram[i].val) {
		if (!first)
		  tbcat(buf, ", ");
		else
		  first = FALSE;
		tbcat(buf, m_type_name(m));
	    }
	}
    }
}

static void
tt_table_row_desc(TextBuffer *buf, int t0, int (*fn)(int i, int j),
		  void (*formatter)(TextBuffer *buf, int val))
{
    int val = (*fn)(t0, 0), val2, t, constant = TRUE, found;
    int i, numentries, first;

    if (formatter == NULL)
      formatter = tb_value_desc;
    if (t_histogram == NULL)
      t_histogram =
	(struct histo *) xmalloc(numttypes * sizeof(struct histo));
    /* Compute a histogram of all the values in the row of the table. */
    numentries = 0;
    t_histogram[numentries].val = val;
    t_histogram[numentries].num = 1;
    ++numentries;
    for_all_terrain_types(t) {
	val2 = (*fn)(t0, t);
	if (val2 == val) {
	    ++(t_histogram[0].num);
	} else {
	    constant = FALSE;
	    found = FALSE;
	    for (i = 1; i < numentries; ++i) {
		if (val2 == t_histogram[i].val) {
		    ++(t_histogram[i].num);
		    found = TRUE;
		    break;
		}
	    }
	    if (!found) {
		t_histogram[numentries].val = val2;
		t_histogram[numentries].num = 1;
		++numentries;
	    }
	}
    }
    /* The constant table/row case is easily disposed of. */
    if (constant) {
	(*formatter)(buf, val);
	tbcat(buf, " for all terrain types");
    	return;
    }
    /* Not a constant row; sort the histogram and compose a description. */
    qsort(t_histogram, numentries, sizeof(struct histo), histogram_compare);
    /* Show a "by default" clause if at least half of the entries are all
       the same. */
    if (t_histogram[0].num * 2 >= numttypes) {
	(*formatter)(buf, t_histogram[0].val);
	tbcat(buf, " by default");
    	i = 1;
    } else {
    	i = 0;
    }
    for (; i < numentries; ++i) {
	if (i > 0)
	  tbcat(buf, ", ");
	(*formatter)(buf, t_histogram[i].val);
	tbcat(buf, " vs ");
	first = TRUE;
	for_all_terrain_types(t) {
	    if ((*fn)(t0, t) == t_histogram[i].val) {
		if (!first)
		  tbcat(buf, ", ");
		else
		  first = FALSE;
		tbcat(buf, t_type_name(t));
	    }
	}
    }
}

#if 0 /* not currently used */
static void
tm_table_row_desc(buf, t0, fn, formatter)
TextBuffer *buf;
int t0;
int (*fn)(int i, int j);
void (*formatter)(TextBuffer *buf, int val);
{
    int val = (*fn)(t0, 0), val2, m, constant = TRUE, found;
    int i, numentries, first;
    struct histo histogram[MAXUTYPES];

    if (formatter == NULL)
      formatter = tb_value_desc;
    /* Compute a histogram of all the values in the row of the table. */
    numentries = 0;
    histogram[numentries].val = val;
    histogram[numentries].num = 1;
    ++numentries;
    for_all_material_types(m) {
	val2 = (*fn)(t0, m);
	if (val2 == val) {
	    ++(histogram[0].num);
	} else {
	    constant = FALSE;
	    found = FALSE;
	    for (i = 1; i < numentries; ++i) {
		if (val2 == histogram[i].val) {
		    ++(histogram[i].num);
		    found = TRUE;
		    break;
		}
	    }
	    if (!found) {
		histogram[numentries].val = val2;
		histogram[numentries].num = 1;
		++numentries;
	    }
	}
    }
    /* The constant table/row case is easily disposed of. */
    if (constant) {
	(*formatter)(buf, val);
	tbcat(buf, " for all material types");
    	return;
    }
    /* Not a constant row; sort the histogram and compose a description. */
    qsort(histogram, numentries, sizeof(struct histo), histogram_compare);
    /* Show a "by default" clause if at least half of the entries are all
       the same. */
    if (histogram[0].num * 2 >= nummtypes) {
	(*formatter)(buf, histogram[0].val);
	tbcat(buf, " by default");
    	i = 1;
    } else {
    	i = 0;
    }
    for (; i < numentries; ++i) {
	if (i > 0)
	  tbcat(buf, ", ");
	(*formatter)(buf, histogram[i].val);
	tbcat(buf, " vs ");
	first = TRUE;
	for_all_material_types(m) {
	    if ((*fn)(t0, m) == histogram[i].val) {
		if (!first)
		  tbcat(buf, ", ");
		else
		  first = FALSE;
		tbcat(buf, m_type_name(m));
	    }
	}
    }
}
#endif

static void
aa_table_row_desc(TextBuffer *buf, int a0, int (*fn)(int i, int j),
		  void (*formatter)(TextBuffer *buf, int val))
{
    aa_table_rowcol_desc(buf, a0, fn, formatter, 0);
}

static void
aa_table_column_desc(TextBuffer *buf, int a1, int (*fn)(int i, int j),
		  void (*formatter)(TextBuffer *buf, int val))
{
    aa_table_rowcol_desc(buf, a1, fn, formatter, 1);
}

static struct histo *a_histogram;

static void
aa_table_rowcol_desc(TextBuffer *buf, int a1, int (*fn)(int i, int j),
		     void (*formatter)(TextBuffer *buf, int val), int rowcol)
{
    int val = (*fn)(0, a1), val2, a, constant = TRUE, found;
    int i, numentries, first;

    if (formatter == NULL)
      formatter = tb_value_desc;
    if (a_histogram == NULL)
      a_histogram =
	(struct histo *) xmalloc(numatypes * sizeof(struct histo));
    val = (rowcol ? (*fn)(0, a1) : (*fn)(a1, 0));
    /* Compute a histogram of all the values in the row of the table. */
    numentries = 0;
    a_histogram[numentries].val = val;
    a_histogram[numentries].num = 1;
    ++numentries;
    for_all_advance_types(a) {
	val2 = (rowcol ? (*fn)(a, a1) : (*fn)(a1, a));
	if (val2 == val) {
	    ++(a_histogram[0].num);
	} else {
	    constant = FALSE;
	    found = FALSE;
	    for (i = 1; i < numentries; ++i) {
		if (val2 == a_histogram[i].val) {
		    ++(a_histogram[i].num);
		    found = TRUE;
		    break;
		}
	    }
	    if (!found) {
		a_histogram[numentries].val = val2;
		a_histogram[numentries].num = 1;
		++numentries;
	    }
	}
    }
    /* The constant table/row case is easily disposed of. */
    if (constant) {
	(*formatter)(buf, val);
	tbcat(buf, " for all advance types");
    	return;
    }
    /* Not a constant column; sort the histogram and compose a description. */
    qsort(a_histogram, numentries, sizeof(struct histo), histogram_compare);
    /* Show a "by default" clause if at least half of the entries are all
       the same. */
    if (a_histogram[0].num * 2 >= numttypes) {
	(*formatter)(buf, a_histogram[0].val);
	tbcat(buf, " by default");
    	i = 1;
    } else {
    	i = 0;
    }
    for (; i < numentries; ++i) {
	if (i > 0)
	  tbcat(buf, ", ");
	(*formatter)(buf, a_histogram[i].val);
	tbcat(buf, " vs ");
	first = TRUE;
	for_all_advance_types(a) {
	    val2 = (rowcol ? (*fn)(a, a1) : (*fn)(a1, a));
	    if (val2 == a_histogram[i].val) {
		if (!first)
		  tbcat(buf, ", ");
		else
		  first = FALSE;
		tbcat(buf, a_type_name(a));
	    }
	}
    }
}

#if 0 /* not currently used */
/* A simple table-printing utility. Blanks out default values so they don't
   clutter the table. */

static void
append_number(buf, value, dflt)
TextBuffer *buf;
int value, dflt;
{
    if (value != dflt) {
	tbprintf(buf, "%5d ", value);
    } else {
	tbprintf(buf, "      ");
    }
}
#endif

static void
append_help_phrase(TextBuffer *buf, char *phrase)
{
    if (empty_string(phrase))
      return;

    /* Extra new line makes display less cluttered. */
    tbcat(buf, "----- ");
    tbcat(buf, phrase);
    tbcat(buf, " -----\n\n");
}

static void
append_notes(TextBuffer *buf, Obj *notes)
{
    char *notestr;
    Obj *rest;

    if (stringp(notes)) {
	notestr = c_string(notes);
	if (strlen(notestr) > 0) { 
	    tbcat(buf, notestr);
	    tbcat(buf, " ");
	} else {
	    tbcat(buf, "\n");
	}
    } else if (consp(notes)) {
	for_all_list(notes, rest) {
	    append_notes(buf, car(rest));
	}
    } else {
	run_warning("notes not list or strings, ignoring");
    }
}

void
notify_instructions(void)
{
    Obj *instructions = mainmodule->instructions, *rest;

    if (instructions != lispnil) {
	if (stringp(instructions)) {
	    notify_all("%s", c_string(instructions));
	} else if (consp(instructions)) {
	    for (rest = instructions; rest != lispnil; rest = cdr(rest)) {
		if (stringp(car(rest))) {
		    notify_all("%s", c_string(car(rest)));
		} else {
		    /* (should probably warn about this case too) */
		}
	    }
	} else {
	    run_warning("Instructions are of wrong type");
	}
    } else {
	notify_all("(no instructions supplied)");
    }
}

/* Print the news file onto the console if there is anything to print. */

void
print_any_news(void)
{
    FILE *fp;

    fp = open_library_file(news_filename());
    if (fp != NULL) {
	printf("\n                              XCONQ NEWS\n\n");
	while (fgets(spbuf, BUFSIZE-1, fp) != NULL) {
	    fputs(spbuf, stdout);
	}
	/* Add another blank line, to separate from init printouts. */
	printf("\n");
	fclose(fp);
    }
}

/* Generate a readable description of the game (design) being played. */
/* This works by writing out appropriate help nodes, along with some
   indexing material.  This does *not* do interface-specific help,
   such as commands. */

void
print_game_description_to_file(FILE *fp)
{
    HelpNode *node;

    /* (need to work on which nodes to dump out) */
    for (node = first_help_node; node != first_help_node; node = node->next) {
	get_help_text(node);
	if (node->text != NULL) {
	    fprintf(fp, "\014\n%s\n", node->key);
	    fprintf(fp, "%s\n", node->text);
	}
    }
}

static void
tb_value_desc(TextBuffer *buf, int val)
{
    char charbuf[30];

    sprintf(charbuf, "%d", val);
    tbcat(buf, charbuf);
}

static void
tb_fraction_desc(TextBuffer *buf, int val)
{
    char charbuf[30];

    if (val % 100 == 0)
      sprintf(charbuf, "%d", val / 100);
    else
      sprintf(charbuf, "%d.%2.2d", val / 100, val % 100);
    tbcat(buf, charbuf);
}

static void
tb_percent_desc(TextBuffer *buf, int val)
{
    tb_value_desc(buf, val);
    tbcat(buf, "%");
}

static void
tb_percent_100th_desc(TextBuffer *buf, int val)
{
    tb_fraction_desc(buf, val);
    tbcat(buf, "%");
}

static void
tb_dice_desc(TextBuffer *buf, int val)
{
    char charbuf[30];

    dice_desc(charbuf, val);
    tbcat(buf, charbuf);
}

static void
tb_mult_desc(TextBuffer *buf, int val)
{
    char charbuf[30];

    sprintf(charbuf, "*%d.%2.2d", val / 100, val % 100);
    tbcat(buf, charbuf);
}

static void
tb_bool_desc(TextBuffer *buf, int val)
{
    tbcat(buf, (val ? "true" : "false"));
}

void
tbprintf(TextBuffer *buf, char *str, ...)
{
    va_list ap;
    char line[300];

    va_start(ap, str);
    vsprintf(line, str, ap);
    tbcat(buf, line);
    va_end(ap);
}

#undef bcopy
#define bcopy(a,b,c) memcpy(b,a,c)

void
tbcat(buf, str)
TextBuffer *buf;
char *str;
{
    obstack_grow(&(buf->ostack), str, strlen(str));
}
