/* Online help support for Xconq.
   Copyright (C) 1987-1989, 1991-1999 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 any_ut_capacity_x PARAMS ((int u));
int any_mp_to_enter_unit PARAMS ((int u));
int any_mp_to_leave_unit PARAMS ((int u));
int any_enter_indep PARAMS ((int u));

/* Obstack allocation and deallocation routines.  */

#define obstack_chunk_alloc xmalloc
#define obstack_chunk_free free

extern int error_is_bug;

#ifndef __STDC__
extern int free ();
#endif

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

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

static int u_property_not_default PARAMS ((int (*fn)(int i), int dflt));
static int t_property_not_default PARAMS ((int (*fn)(int i), int dflt));
static int m_property_not_default PARAMS ((int (*fn)(int i), int dflt));
static int uu_table_row_not_default PARAMS ((int u, int (*fn)(int i, int j), int dflt));
static int ut_table_row_not_default PARAMS ((int u, int (*fn)(int i, int j), int dflt));
static int um_table_row_not_default PARAMS ((int u, int (*fn)(int i, int j), int dflt));
static int tt_table_row_not_default PARAMS ((int t, int (*fn)(int i, int j), int dflt));
static int tm_table_row_not_default PARAMS ((int t, int (*fn)(int i, int j), int dflt));
static int aa_table_row_not_default PARAMS ((int a1, int (*fn)(int i, int j), int dflt));
static void u_property_desc PARAMS ((TextBuffer *buf, int (*fn)(int), void (*formatter)(TextBuffer *, int)));
static void t_property_desc PARAMS ((TextBuffer *buf, int (*fn)(int), void (*formatter)(TextBuffer *, int)));
static void m_property_desc PARAMS ((TextBuffer *buf, int (*fn)(int), void (*formatter)(TextBuffer *, int)));
static void uu_table_row_desc PARAMS ((TextBuffer *buf, int u, int (*fn)(int, int), void (*formatter)(TextBuffer *, int), char *connect));
static void ut_table_row_desc PARAMS ((TextBuffer *buf, int u, int (*fn)(int, int), void (*formatter)(TextBuffer *, int), char *connect));
static void um_table_row_desc PARAMS ((TextBuffer *buf, int u, int (*fn)(int, int), void (*formatter)(TextBuffer *, int)));
static void tt_table_row_desc PARAMS ((TextBuffer *buf, int t1, int (*fn)(int, int), void (*formatter)(TextBuffer *, int)));
static void tm_table_row_desc PARAMS ((TextBuffer *buf, int t, int (*fn)(int, int), void (*formatter)(TextBuffer *, int)));
static void aa_table_row_desc PARAMS ((TextBuffer *buf, int a1, int (*fn)(int, int), void (*formatter)(TextBuffer *, int)));
static void tb_value_desc PARAMS ((TextBuffer *buf, int val));
static void tb_percent_desc PARAMS ((TextBuffer *buf, int val));
static void tb_percent_100th_desc PARAMS ((TextBuffer *buf, int val));
static void tb_dice_desc PARAMS ((TextBuffer *buf, int val));
static void append_number PARAMS ((TextBuffer *buf, int value, int dflt));
static void append_help_phrase PARAMS ((TextBuffer *buf, char *phrase));
static void append_notes PARAMS ((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()
{
    /* 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()
{
    int u, m, t, a;
    char *name, *longname;
    HelpNode *node;

    add_help_node("instructions", describe_instructions, 0, NULL);
    add_help_node("game design", 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()
{
    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(key, fn, arg, prevnode)
char *key;
void (*fn)();
int arg;
HelpNode *prevnode;
{
    HelpNode *node, *nextnode;

    if (empty_string(key)) {
	error_is_bug = TRUE;
	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(node, str)
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(node)
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(arg, key, buf)
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(arg, key, buf)
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(arg, key, buf)
int arg;
char *key;
TextBuffer *buf;
{
    FILE *fp;

    fp = fopen(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(arg, key, buf)
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(arg, key, buf)
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(arg, key, buf)
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, "The game is \"%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);
    }
    tbcat(buf, ".\n");
    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 (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 (any_temp_variation) {
	tbprintf(buf, "Lowest possible temperature is %d, at an elevation of %d.\n",
		 g_temp_floor(), g_temp_floor_elev());
    }
    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_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_char(a)))
		  tbprintf(buf, "char '%s'", a_char(a));
		else
		  tbcat(buf, "no char");
		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) {
	tbprintf(buf, "Scorefile name is \"%s\".\n", g_scorefile_name());
    }
#endif /* DESIGNERS */
}

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

static void
describe_game_modules(arg, key, buf)
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(buf, module, level)
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          %s\n",
	    indentbuf,
	    (module->blurb ? module->blurb : "(no description)"));
    if (module->notes != lispnil) {
	tbprintf(buf, "%s          (See notes below)\n", indentbuf);
    }
    /* Now describe any included modules. */
    for_all_includes(module, submodule) {
	describe_game_module_aux(buf, submodule, level + 1);
    }
}

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

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

    if (module->notes != lispnil) {
	tbprintf(buf, "\nNotes to \"%s\":\n", module->name);
	append_notes(buf, module->notes);
    }
#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(u)
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(u)
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(u)
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(u)
int u;
{
    int u2;
	
    for_all_unit_types(u2) {
	if (uu_can_enter_indep(u, u2))
	  return TRUE;
    }
    return FALSE;
}

void fraction_desc PARAMS ((TextBuffer *buf, int n));

void
fraction_desc(buf, n)
TextBuffer *buf;
int n;
{
    tbprintf(buf, "%d.%d", n / 100, n % 100);
}

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

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

static void
describe_utype(u, key, buf)
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));
    }
    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_plus_indep(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, NULL, NULL);
	    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 ");
	    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 ");
	    fraction_desc(buf, u_speed_min(u));
	    tbcat(buf, " and ");
	    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 ");
	fraction_desc(buf, u_hp_recovery(u));
	tbprintf(buf, " HP each turn.");
    }
    tbcat(buf, "\n");
    /* (should only describe if any occ possibilities?) */
    if (u_capacity(u) != 0) {
	tbprintf(buf, "Generic capacity for units is %d.\n",
		 u_capacity(u));
    }
    /* (should list size of units when in this one) */
    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 (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_research(u) > 0
        ) {
        tbcat(buf, "\nResearch:\n");
	tbcat(buf, "ACP to research: ");
        uu_table_row_desc(buf, u, uu_acp_to_research, NULL, NULL);
	tbcat(buf, ".\n");
	tbcat(buf, "  Tech gained: ");
        uu_table_row_desc(buf, u, uu_tech_per_research, 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));
        }
        /* 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, NULL);
	    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, NULL);
		tbcat(buf, ".\n");
		tbcat(buf, "Attack range min is ");
		uu_table_row_desc(buf, u, uu_attack_range_min, NULL, NULL);
		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");
	    }
   	}
	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 (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, "Can never reduce defender's HP below ");
	    uu_table_row_desc(buf, u, uu_hp_min, NULL, NULL);
	    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, NULL, NULL);
	    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 (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 change type 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_acp_to_remove_terrain, 0)) {
	    tbcat(buf, "ACP to change type is ");
	    ut_table_row_desc(buf, u, ut_acp_to_remove_terrain, 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 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_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");
	    /* (should not be here, because might be some other unit
               doing the LOS viewing) */
	    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_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_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 (nummtypes > 0) {
	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 base production", um_base_production(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 initially)", min(um_initial(u, m), um_storage_x(u, m)));
		}
		usesm = TRUE;
	    }
	    if (um_base_consumption(u, m) > 0) {
		tbprintf(buf, ", %d base 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_consumption_per_move(u, m) != 0) {
		tbprintf(buf, ", %d consumed per move",
			 um_consumption_per_move(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 (um_consumption_on_creation(u, m) != 0) {
		tbprintf(buf, ", %d needed to create",
			 um_consumption_on_creation(u, m));
		usesm = TRUE;
	    }
	    if (um_consumption_per_build(u, m) != 0) {
		tbprintf(buf, ", %d needed to add 1 CP",
			 um_consumption_per_build(u, m));
		usesm = TRUE;
	    }
	    if (um_consumption_per_repair(u, m) != 0) {
		tbprintf(buf, ", %d needed to restore 1 HP",
			 um_consumption_per_repair(u, m));
		usesm = TRUE;
	    }
	    if (usesm) {
		if (um_inlength(u, m) > 0) {
		    tbprintf(buf, ", receive from %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");
	}
    }
    if (numatypes > 0) {
	tbcat(buf, "\nAdvances:\n");
	for_all_advance_types(a) {
	    if (ua_needed_to_build(u, a) > 0) {
		tbprintf(buf, "  %s", a_type_name(a));
		if (ua_needed_to_build(u, a) > 0) {
		    tbprintf(buf, ", needed to build unit");
		}
		/* (should add other unit-advance tables here) */
		tbcat(buf, "\n");
	    }
	}
    }
    /* (should display weather interaction here) */
    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 */) {
	fraction_desc(buf, u_revolt(u));
	tbprintf(buf, "%% chance of revolt.\n");
    }
    /* Display the designer's notes for this type. */
    if (u_notes(u) != lispnil) {
	tbcat(buf, "\nNotes:\n");
	append_notes(buf, u_notes(u));
    }
}

int
may_detonate(u)
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(m, key, buf)
int m;
char *key;
TextBuffer *buf;
{
    append_help_phrase(buf, m_help(m));
    if (m_people(m) > 0) {
	tbprintf(buf, "1 of this represents %d individuals.", m_people(m));
    }
    /* (should display unit columns here) */
    /* Display the designer's notes for this type. */
    if (m_notes(m) != lispnil) {
	tbcat(buf, "\nNotes:\n");
	append_notes(buf, m_notes(m));
    }
}

static void
describe_atype(a, key, buf)
int a;
char *key;
TextBuffer *buf;
{
    int a2;

    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, "Needed to start research: ");
    aa_table_row_desc(buf, a, aa_needed_to_research, NULL);
    tbcat(buf, ".\n");
}

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

    append_help_phrase(buf, t_help(t));
    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;
    }
    tbprintf(buf, "Generic unit capacity is %d.\n", t_capacity(t));
    if (minelev != maxelev /* should be "elevscanvary" */) {
	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 (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) {
	    	tbprintf(buf, "Can store up to %d %s", tm_storage_x(t, m), m_type_name(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))
			 );
	    }
	}
    }
    /* Display the designer's notes for this type. */
    if (t_notes(t) != lispnil) {
	tbcat(buf, "\nNotes:\n");
	append_notes(buf, t_notes(t));
    }
}

static void
describe_scorekeepers(arg, key, buf)
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(arg, key, buf)
int arg;
char *key;
TextBuffer *buf;
{
    int u, t, 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, NULL);
			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");
		    }
		}
		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()));
		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()));
		break;
	      case K_MAKE_INDEPENDENT_UNITS:
		tbcat(buf, "\nIndependent units:");
		describe_synth_run(buf, methkey);
		tbcat(buf, "\n");
		/* (should show indep density and 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");
		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, NULL);
		    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 type.\n");
		}
		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, within %d of road.\n",
				 u_spur_chance(u), u_spur_range(u));
		    }
		}
		/* (should include routing terrain preferences) */
		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 for side info.\n", length(g_side_lib()));
}

static void
describe_synth_run(buf, methkey)
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(arg, key, buf)
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 (world.yearlength > 1) {
	tbprintf(buf, "Length of year: %d turns.  ", 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);
    }
    /* (should describe temperature year cycle here) */
#ifdef DESIGNERS
    if (numdesigners > 0) {
	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 */
}

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

void
describe_command (ch, name, help, onechar, buf)
int ch, onechar;
char *name, *help;
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(fn, dflt)
int (*fn) PARAMS ((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(fn, dflt)
int (*fn) PARAMS ((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) PARAMS ((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) PARAMS ((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) PARAMS ((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) PARAMS ((int i, int j));
{
    int t2, val2;

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

static int
tm_table_row_not_default(t, fn, dflt)
int t, dflt;
int (*fn) PARAMS ((int i, int j));
{
    int m, val2;

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

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

    for_all_advance_types(a2) {
	val2 = (*fn)(a1, a2);
	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(h1, h2)
CONST void *h1, *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 void
u_property_desc(buf, fn, formatter)
TextBuffer *buf;
int (*fn) PARAMS ((int i));
void (*formatter) PARAMS ((TextBuffer *buf, int val));
{
    int val, u, val2, 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 for the given property. */
    numentries = 0;
    val = (*fn)(0);
    histogram[numentries].val = val;
    histogram[numentries].num = 1;
    ++numentries;
    for_all_unit_types(u) {
	val2 = (*fn)(u);
	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 unit 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 >= numutypes) {
	(*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, " for ");
	first = TRUE;
	for_all_unit_types(u) {
	    if ((*fn)(u) == histogram[i].val) {
		if (!first)
		  tbcat(buf, ",");
		else
		  first = FALSE;
		tbcat(buf, u_type_name(u));
	    }
	}
    }
}

static void
t_property_desc(buf, fn, formatter)
TextBuffer *buf;
int (*fn) PARAMS ((int i));
void (*formatter) PARAMS ((TextBuffer *buf, int val));
{
    int val, t, val2, 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 for the given property. */
    numentries = 0;
    val = (*fn)(0);
    histogram[numentries].val = val;
    histogram[numentries].num = 1;
    ++numentries;
    for_all_terrain_types(t) {
	val2 = (*fn)(t);
	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 terrain 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 >= numttypes) {
	(*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, " for ");
	first = TRUE;
	for_all_terrain_types(t) {
	    if ((*fn)(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(buf, u, fn, formatter, connect)
TextBuffer *buf;
int u;
int (*fn) PARAMS ((int i, int j));
void (*formatter) PARAMS ((TextBuffer *buf, int val));
char *connect;
{
    int val = (*fn)(u, 0), val2, u2, constant = TRUE, found;
    int i, numentries, first;
    struct histo histogram[MAXUTYPES];

    if (formatter == NULL)
      formatter = tb_value_desc;
    if (empty_string(connect))
      connect = "for";
    /* 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_unit_types(u2) {
	val2 = (*fn)(u, u2);
	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);
	tbprintf(buf, " %s all unit types", connect);
    	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 >= numutypes) {
	(*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);
	tbprintf(buf, " %s ", connect);
	first = TRUE;
	for_all_unit_types(u2) {
	    if ((*fn)(u, u2) == 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(buf, u, fn, formatter, connect)
TextBuffer *buf;
int u;
int (*fn) PARAMS ((int i, int j));
void (*formatter) PARAMS ((TextBuffer *buf, int val));
char *connect;
{
    int val = (*fn)(u, 0), val2, t, constant = TRUE, found;
    int i, numentries, first;
    struct histo histogram[MAXUTYPES];

    if (formatter == NULL)
      formatter = tb_value_desc;
    if (empty_string(connect))
      connect = "for";
    /* 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_terrain_types(t) {
	val2 = (*fn)(u, t);
	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);
	tbprintf(buf, " %s all terrain types", connect);
    	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 >= numttypes) {
	(*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);
	tbprintf(buf, " %s ", connect);
	first = TRUE;
	for_all_terrain_types(t) {
	    if ((*fn)(u, t) == histogram[i].val) {
		if (!first)
		  tbcat(buf, ",");
		else
		  first = FALSE;
		tbcat(buf, t_type_name(t));
	    }
	}
    }
}

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

    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)(u, 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);
	tbprintf(buf, " %s all material types", connect);
    	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);
	tbprintf(buf, " %s ", connect);
	first = TRUE;
	for_all_material_types(m) {
	    if ((*fn)(u, m) == histogram[i].val) {
		if (!first)
		  tbcat(buf, ",");
		else
		  first = FALSE;
		tbcat(buf, m_type_name(m));
	    }
	}
    }
}

static void
tt_table_row_desc(buf, t0, fn, formatter)
TextBuffer *buf;
int t0;
int (*fn) PARAMS ((int i, int j));
void (*formatter) PARAMS ((TextBuffer *buf, int val));
{
    int val = (*fn)(t0, 0), val2, t, 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_terrain_types(t) {
	val2 = (*fn)(t0, t);
	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 terrain 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 >= numttypes) {
	(*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_terrain_types(t) {
	    if ((*fn)(t0, t) == histogram[i].val) {
		if (!first)
		  tbcat(buf, ",");
		else
		  first = FALSE;
		tbcat(buf, t_type_name(t));
	    }
	}
    }
}

static void
tm_table_row_desc(buf, t0, fn, formatter)
TextBuffer *buf;
int t0;
int (*fn) PARAMS ((int i, int j));
void (*formatter) PARAMS ((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));
	    }
	}
    }
}

static void
aa_table_row_desc(buf, a0, fn, formatter)
TextBuffer *buf;
int a0;
int (*fn) PARAMS ((int i, int j));
void (*formatter) PARAMS ((TextBuffer *buf, int val));
{
    int val = (*fn)(a0, 0), val2, a, constant = TRUE, found;
    int i, numentries, first;
    struct histo histogram[MAXATYPES];

    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_advance_types(a) {
	val2 = (*fn)(a0, a);
	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 advance 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 >= numttypes) {
	(*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_advance_types(a) {
	    if ((*fn)(a0, a) == histogram[i].val) {
		if (!first)
		  tbcat(buf, ",");
		else
		  first = FALSE;
		tbcat(buf, a_type_name(a));
	    }
	}
    }
}

/* A simple table-printing utility. Blanks out default values so they don't
   clutter the table. */
/* (not currently used anywhere?) */

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

static void
append_help_phrase(buf, phrase)
TextBuffer *buf;
char *phrase;
{
    if (empty_string(phrase))
      return;
    tbcat(buf, "-- ");
    tbcat(buf, phrase);
    tbcat(buf, " --\n");
}

static void
append_notes(buf, 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()
{
    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()
{
    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(fp)
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(buf, val)
TextBuffer *buf;
int val;
{
    char charbuf[30];

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

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

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

static void
tb_percent_100th_desc(buf, val)
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_dice_desc(buf, val)
TextBuffer *buf;
int val;
{
    char charbuf[30];

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

#ifdef __STDC__
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);
}
#else
void
tbprintf(buf, str, a1, a2, a3, a4, a5, a6, a7, a8, a9)
TextBuffer *buf;
char *str;
long a1, a2, a3, a4, a5, a6, a7, a8, a9;
{
    char line[300];

    sprintf(line, str, a1, a2, a3, a4, a5, a6, a7, a8, a9);
    tbcat(buf, line);
}
#endif

#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));
}
