/* playerdb.c
 *
 */

/*
    fics - An internet chess server.
    Copyright (C) 1993  Richard V. Nash

    This program 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 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.
*/

/* Revision history:
   name		email		yy/mm/dd	Change
   Richard Nash                 93/10/22	Created
   Markus Uhlin                 23/12/13	Fixed compiler warnings
   Markus Uhlin                 23/12/17	Usage of 'time_t'
   Markus Uhlin                 23/12/19	Usage of 'time_t'
   Markus Uhlin                 23/12/25	Reformatted functions
   Markus Uhlin                 23/12/31	Completed reformation of all
						functions and much more...
   Markus Uhlin                 24/03/16	Replaced unbounded string
						handling functions and added
						truncation checks.
   Markus Uhlin                 24/08/04	Fixed multiple possible buffer
						overflows.
   Markus Uhlin                 24/08/13	Handled function return values
   Markus Uhlin                 24/11/24	Fixed incorrect format strings
   Markus Uhlin                 24/12/02	Made many improvements
   Markus Uhlin                 24/12/04	Added player number checks
*/

#include "stdinclude.h"
#include "common.h"

#include <err.h>
#include <stdint.h>

#include "command.h"
#include "comproc.h"
#include "config.h"
#include "ficslim.h"
#include "ficsmain.h"
#include "gamedb.h"
#include "lists.h"
#include "network.h"
#include "playerdb.h"
#include "ratings.h"
#include "rmalloc.h"
#include "talkproc.h"
#include "utils.h"

#if __linux__
#include <bsd/string.h>
#endif

PUBLIC player	 parray[PARRAY_SIZE];
PUBLIC int	 p_num = 0;

/*
 * Checks if a player number is within bounds.
 */
PUBLIC bool
player_num_ok_chk(const int num)
{
	return (num >= 0 && num <= p_num &&
	    num < (int)ARRAY_SIZE(parray));
}

PRIVATE int
get_empty_slot(void)
{
	for (int i = 0; i < p_num; i++) {
		if (parray[i].status == PLAYER_EMPTY)
			return i;
	}

	p_num++;

	if ((p_num + 1) >= PARRAY_SIZE) {
		fprintf(stderr, "*** Bogus attempt to %s() past end of parray "
		    "***\n", __func__);
	}

	parray[p_num - 1].status = PLAYER_EMPTY;

	return (p_num - 1);
}

PUBLIC void
player_array_init(void)
{
	for (int i = 0; i < PARRAY_SIZE; i++)
		parray[i].status = PLAYER_EMPTY;
}

PUBLIC void
player_init(int startConsole)
{
	int p;

	if (startConsole) {
		net_addConnection(0, 0);
		p = player_new();

		parray[p].login		= xstrdup("console");
		parray[p].name		= xstrdup("console");
		parray[p].passwd	= xstrdup("*");
		parray[p].fullName	= xstrdup("The Operator");
		parray[p].emailAddress	= NULL;
		parray[p].prompt	= xstrdup("fics%");
		parray[p].adminLevel	= ADMIN_GOD;
		parray[p].socket	= 0;
		parray[p].busy[0]	= '\0';

		pprintf_prompt(p, "\nLogged in on console.\n");
	}
}

PUBLIC int
player_new(void)
{
	int new;

	new = get_empty_slot();
	player_zero(new);
	return new;
}

PUBLIC int
player_zero(int p)
{
#define INVALID ((char *)-42)
	int i;

	parray[p].name		= NULL;
	parray[p].emailAddress	= NULL;
	parray[p].fullName	= NULL;
	parray[p].passwd	= NULL;
	parray[p].prompt	= def_prompt;

	parray[p].adminLevel = 0;
	parray[p].automail = 0;

	for (i = 0; i < MAX_ALIASES; i++) {
		parray[p].alias_list[i].comm_name = INVALID;
		parray[p].alias_list[i].alias = INVALID;
	}

	parray[p].bell		= 0;
	parray[p].d_height	= 24;
	parray[p].d_inc		= 12;
	parray[p].d_time	= 2;
	parray[p].d_width	= 79;

	parray[p].flip		= 0;
	parray[p].formula	= NULL;

	for (i = 0; i < MAX_FORMULA; i++)
		parray[p].formulaLines[i] = NULL;

	parray[p].game		= -1;
	parray[p].highlight	= 0;
	parray[p].i_admin	= 1;
	parray[p].i_cshout	= 1;
	parray[p].i_game	= 0;
	parray[p].i_kibitz	= 1;
	parray[p].i_login	= 0;
	parray[p].i_mailmess	= 0;
	parray[p].i_shout	= 1;
	parray[p].i_tell	= 1;
	parray[p].jprivate	= 0;
	parray[p].kiblevel	= 0;
	parray[p].language	= LANG_DEFAULT;

	parray[p].lastColor		= WHITE;
	parray[p].lastHost		= 0;
	parray[p].last_channel		= -1;
	parray[p].last_command_time	= 0;
	parray[p].last_file		= NULL;
	parray[p].last_file_byte	= 0L;
	parray[p].last_opponent		= -1;
	parray[p].last_tell		= -1;
	parray[p].lastshout_a		= 0;
	parray[p].lastshout_b		= 0;

	parray[p].lists		= NULL;
	parray[p].login		= NULL;
	parray[p].logon_time	= 0;
	parray[p].notifiedby	= 0;
	parray[p].numAlias	= 0;
	parray[p].num_black	= 0;
	parray[p].num_comments	= 0;
	parray[p].num_formula	= 0;
	parray[p].num_from	= 0;
	parray[p].num_observe	= 0;
	parray[p].num_plan	= 0;
	parray[p].num_to	= 0;
	parray[p].num_white	= 0;
	parray[p].open		= 1;
	parray[p].opponent	= -1;

	parray[p].partner = -1;
	parray[p].pgn = 0;
	for (i = 0; i < MAX_PLAN; i++)
		parray[p].planLines[i] = INVALID;
	parray[p].private = 0;
	parray[p].promote = QUEEN;

	parray[p].rated			= 0;
	parray[p].registered		= 0;
	parray[p].ropen			= 1;
	parray[p].seek			= 0;
	parray[p].simul_info.numBoards	= 0;
	parray[p].socket		= -1;
	parray[p].sopen			= 0;
	parray[p].status		= PLAYER_NEW;
	parray[p].style			= 0;
	parray[p].thisHost		= 0;
	parray[p].timeOfReg		= 0;
	parray[p].totalTime		= 0;

	parray[p].b_stats.best		= 0;
	parray[p].b_stats.dra		= 0;
	parray[p].b_stats.los		= 0;
	parray[p].b_stats.ltime		= 0;
	parray[p].b_stats.num		= 0;
	parray[p].b_stats.rating	= 0;
	parray[p].b_stats.sterr		= 350.0;
	parray[p].b_stats.whenbest	= 0;
	parray[p].b_stats.win = 0;

	parray[p].bug_stats.best	= 0;
	parray[p].bug_stats.dra		= 0;
	parray[p].bug_stats.los		= 0;
	parray[p].bug_stats.ltime	= 0;
	parray[p].bug_stats.num		= 0;
	parray[p].bug_stats.rating	= 0;
	parray[p].bug_stats.sterr	= 350.0;
	parray[p].bug_stats.whenbest	= 0;
	parray[p].bug_stats.win		= 0;

	parray[p].l_stats.best		= 0;
	parray[p].l_stats.dra		= 0;
	parray[p].l_stats.los		= 0;
	parray[p].l_stats.ltime		= 0;
	parray[p].l_stats.num		= 0;
	parray[p].l_stats.rating	= 0;
	parray[p].l_stats.sterr		= 350.0;
	parray[p].l_stats.whenbest	= 0;
	parray[p].l_stats.win		= 0;

	parray[p].s_stats.best		= 0;
	parray[p].s_stats.dra		= 0;
	parray[p].s_stats.los		= 0;
	parray[p].s_stats.ltime		= 0;
	parray[p].s_stats.num		= 0;
	parray[p].s_stats.rating	= 0;
	parray[p].s_stats.sterr		= 350.0;
	parray[p].s_stats.whenbest	= 0;
	parray[p].s_stats.win		= 0;

	parray[p].w_stats.best		= 0;
	parray[p].w_stats.dra		= 0;
	parray[p].w_stats.los		= 0;
	parray[p].w_stats.ltime		= 0;
	parray[p].w_stats.num		= 0;
	parray[p].w_stats.rating	= 0;
	parray[p].w_stats.sterr		= 350.0;
	parray[p].w_stats.whenbest	= 0;
	parray[p].w_stats.win		= 0;

	// Used to store the player's interface.
	// For example: xboard 4.9.1
	(void) memset(&(parray[p].interface[0]), 0,
	    ARRAY_SIZE(parray[p].interface));

	return 0;
}

PUBLIC int
player_free(int p)
{
	int i;

	strfree(parray[p].login);
	strfree(parray[p].name);
	strfree(parray[p].passwd);
	strfree(parray[p].fullName);
	strfree(parray[p].emailAddress);

	if (parray[p].prompt != def_prompt)
		strfree(parray[p].prompt);

	for (i = 0; i < parray[p].num_plan; i++)
		strfree(parray[p].planLines[i]);
	for (i = 0; i < parray[p].num_formula; i++)
		strfree(parray[p].formulaLines[i]);

	strfree(parray[p].formula);
	list_free(parray[p].lists);

	for (i = 0; i < parray[p].numAlias; i++) {
		strfree(parray[p].alias_list[i].comm_name);
		strfree(parray[p].alias_list[i].alias);
	}

	return 0;
}

PUBLIC int
player_clear(int p)
{
	player_free(p);
	player_zero(p);
	return 0;
}

PUBLIC int
player_remove(int p)
{
	int i;

	if (!player_num_ok_chk(p)) {
		warnx("%s: invalid player number %d", __func__, p);
		return -1;
	}

	player_decline_offers(p, -1, -1);
	player_withdraw_offers(p, -1, -1);

	if (parray[p].simul_info.numBoards) {	// Player disconnected in
						// middle of simul
		for (i = 0; i < parray[p].simul_info.numBoards; i++) {
			if (parray[p].simul_info.boards[i] >= 0) {
				game_disconnect(parray[p].simul_info.boards[i],
				    p);
			}
		}
	}

	if (parray[p].game >= 0) {	// Player disconnected in the middle of
					// a game!
		pprintf(parray[p].opponent, "Your opponent has lost contact "
		    "or quit.\n");
		game_disconnect(parray[p].game, p);
	}

	for (i = 0; i < p_num; i++) {
		if (parray[i].status == PLAYER_EMPTY)
			continue;

		if (parray[i].last_tell == p)
			parray[i].last_tell = -1;

		if (parray[i].last_opponent == p)
			parray[i].last_opponent = -1;

		if (parray[i].partner == p) {
			pprintf_prompt(i, "Your partner has disconnected.\n");
			player_withdraw_offers(i, -1, PEND_BUGHOUSE);
			player_decline_offers(i, -1, PEND_BUGHOUSE);
			parray[i].partner = -1;
		}
	}

	player_clear(p);
	parray[p].status = PLAYER_EMPTY;
	return 0;
}

PRIVATE int
add_to_list(FILE *fp, enum ListWhich lw, int *size, int p)
{
	char buf[MAX_STRING_LENGTH] = { '\0' };

	_Static_assert(1023 < ARRAY_SIZE(buf), "Buffer too small");

#define SCAN_STR "%1023s"

	if (*size <= 0)
		return -2;
	while ((*size)-- > 0 && fscanf(fp, SCAN_STR, buf) == 1)
		list_add(p, lw, buf);
	return (*size <= 0 ? 0 : -1);
}

PRIVATE void
ReadV1PlayerFmt(int p, player *pp, FILE *fp, char *file, int version)
{
	char		*tmp;
	char		 tmp2[MAX_STRING_LENGTH] = { '\0' };
	int		 bs, ss, ws, ls, bugs;
	int		 i, size_cens, size_noplay, size_not, size_gnot,
			 size_chan, len;
	intmax_t	 array[2] = { 0 };
	intmax_t	 ltime_tmp[5] = { 0 };
	intmax_t	 wb_tmp[5] = { 0 };
	size_t		 n;

	/* XXX: not referenced */
	(void) version;

	/*
	 * Name
	 */
	if (fgets(tmp2, sizeof tmp2, fp) != NULL &&
	    strcmp(tmp2, "NONE\n") != 0) {
		tmp2[strcspn(tmp2, "\n")] = '\0';
		pp->name = xstrdup(tmp2);
	} else {
		pp->name = NULL;
	}

	/*
	 * Full name
	 */
	if (fgets(tmp2, sizeof tmp2, fp) != NULL &&
	     strcmp(tmp2, "NONE\n") != 0) {
		tmp2[strcspn(tmp2, "\n")] = '\0';
		pp->fullName = xstrdup(tmp2);
	} else {
		pp->fullName = NULL;
	}

	/*
	 * Password
	 */
	if (fgets(tmp2, sizeof tmp2, fp) != NULL &&
	    strcmp(tmp2, "NONE\n") != 0) {
		tmp2[strcspn(tmp2, "\n")] = '\0';
		pp->passwd = xstrdup(tmp2);
	} else {
		pp->passwd = NULL;
	}

	/*
	 * Email
	 */
	if (fgets(tmp2, sizeof tmp2, fp) != NULL &&
	    strcmp(tmp2, "NONE\n") != 0) {
		tmp2[strcspn(tmp2, "\n")] = '\0';
		pp->emailAddress = xstrdup(tmp2);
	} else {
		pp->emailAddress = NULL;
	}

	if (fscanf(fp, "%d %d %d %d %d %d %jd %d %jd %d %d %d %d %d %d %jd %d %jd "
	    "%d %d %d %d %d %d %jd %d %jd %d %d %d %d %d %d %jd %d %jd %d %d %d %d "
	    "%d %d %jd %d %jd %u\n",
	    &pp->s_stats.num, &pp->s_stats.win, &pp->s_stats.los,
	    &pp->s_stats.dra, &pp->s_stats.rating, &ss,
	    &ltime_tmp[0], &pp->s_stats.best, &wb_tmp[0],

	    &pp->b_stats.num, &pp->b_stats.win, &pp->b_stats.los,
	    &pp->b_stats.dra, &pp->b_stats.rating, &bs,
	    &ltime_tmp[1], &pp->b_stats.best, &wb_tmp[1],

	    &pp->w_stats.num, &pp->w_stats.win, &pp->w_stats.los,
	    &pp->w_stats.dra, &pp->w_stats.rating, &ws,
	    &ltime_tmp[2], &pp->w_stats.best, &wb_tmp[2],

	    &pp->l_stats.num, &pp->l_stats.win, &pp->l_stats.los,
	    &pp->l_stats.dra, &pp->l_stats.rating, &ls,
	    &ltime_tmp[3], &pp->l_stats.best, &wb_tmp[3],

	    &pp->bug_stats.num, &pp->bug_stats.win, &pp->bug_stats.los,
	    &pp->bug_stats.dra, &pp->bug_stats.rating, &bugs,
	    &ltime_tmp[4], &pp->bug_stats.best, &wb_tmp[4],

	    &pp->lastHost) != 46) {
		fprintf(stderr, "Player %s is corrupt\n", parray[p].name);
		return;
	}

	for (n = 0; n < ARRAY_SIZE(ltime_tmp); n++) {
		if (ltime_tmp[n] < g_time_min ||
		    ltime_tmp[n] > g_time_max) {
			warnx("%s: player %s is corrupt "
			    "('ltime' out of bounds!)",
			    __func__, parray[p].name);
			return;
		}
	}

	pp->s_stats.ltime = ltime_tmp[0];
	pp->b_stats.ltime = ltime_tmp[1];
	pp->w_stats.ltime = ltime_tmp[2];
	pp->l_stats.ltime = ltime_tmp[3];
	pp->bug_stats.ltime = ltime_tmp[4];

	for (n = 0; n < ARRAY_SIZE(wb_tmp); n++) {
		if (wb_tmp[n] < g_time_min ||
		    wb_tmp[n] > g_time_max) {
			warnx("%s: player %s is corrupt "
			    "('whenbest' out of bounds!)",
			    __func__, parray[p].name);
			return;
		}
	}

	pp->s_stats.whenbest = wb_tmp[0];
	pp->b_stats.whenbest = wb_tmp[1];
	pp->w_stats.whenbest = wb_tmp[2];
	pp->l_stats.whenbest = wb_tmp[3];
	pp->bug_stats.whenbest = wb_tmp[4];

	pp->b_stats.sterr	= (bs / 10.0);
	pp->s_stats.sterr	= (ss / 10.0);
	pp->w_stats.sterr	= (ws / 10.0);
	pp->l_stats.sterr	= (ls / 10.0);
	pp->bug_stats.sterr	= (bugs / 10.0);

	if (fgets(tmp2, sizeof tmp2, fp) == NULL) {
		fprintf(stderr, "Player %s is corrupt\n", parray[p].name);
		return;
	} else {
		tmp2[strcspn(tmp2, "\n")] = '\0';
		pp->prompt = xstrdup(tmp2);
	}

	if (fscanf(fp, "%d %d %d %jd %jd %d %d %d %d %d %d %d %d %d %d %d %d %d "
	    "%d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d\n",
	    &pp->open, &pp->rated, &pp->ropen, &array[0], &array[1],
	    &pp->bell, &pp->pgn, &pp->notifiedby, &pp->i_login, &pp->i_game,
	    &pp->i_shout, &pp->i_cshout, &pp->i_tell, &pp->i_kibitz,
	    &pp->private, &pp->jprivate, &pp->automail, &pp->i_mailmess,
	    &pp->style, &pp->d_time, &pp->d_inc, &pp->d_height, &pp->d_width,
	    &pp->language, &pp->adminLevel, &pp->num_white, &pp->num_black,
	    &pp->highlight, &pp->num_comments, &pp->num_plan, &pp->num_formula,
	    &size_cens, &size_not, &size_noplay, &size_gnot, &pp->numAlias,
	    &size_chan) != 37) {
		fprintf(stderr, "Player %s is corrupt\n", parray[p].name);
		return;
	}

	for (n = 0; n < ARRAY_SIZE(array); n++) {
		if (array[n] < g_time_min ||
		    array[n] > g_time_max) {
			warnx("%s: player %s is corrupt "
			    "(time not within range)",
			    __func__, parray[p].name);
			return;
		}
	}

	pp->timeOfReg = array[0];
	pp->totalTime = array[1];

	if (pp->num_plan > 0) {
		for (i = 0; i < pp->num_plan; i++) {
			if (fgets(tmp2, sizeof tmp2, fp) == NULL) {
				warnx("%s: bad plan: feof %s", __func__, file);
				return;
			}

			if (!(len = strlen(tmp2))) {
				fprintf(stderr, "FICS: Error bad plan in "
				    "file %s\n", file);
				i--;
				pp->num_plan--;
			} else {
				// Get rid of '\n'.
				tmp2[strcspn(tmp2, "\n")] = '\0';

				pp->planLines[i] = (len > 1 ? xstrdup(tmp2) :
				    NULL);
			}
		}
	}

	if (pp->num_formula > 0) {
		for (i = 0; i < pp->num_formula; i++) {
			if (fgets(tmp2, sizeof tmp2, fp) == NULL) {
				warnx("%s: bad formula: feof %s", __func__,
				    file);
				return;
			}

			if (!(len = strlen(tmp2))) {
				fprintf(stderr, "FICS: Error bad formula in "
				    "file %s\n", file);
				i--;
				pp->num_formula--;
			} else {
				// Get rid of '\n'.
				tmp2[strcspn(tmp2, "\n")] = '\0';

				pp->formulaLines[i] = (len > 1 ? xstrdup(tmp2) :
				    NULL);
			}
		}
	}

	if (fgets(tmp2, sizeof tmp2, fp) == NULL) {
		warnx("%s: fgets() error", __func__);
		return;
	}

	tmp2[strcspn(tmp2, "\n")] = '\0';

	if (!strcmp(tmp2, "NONE"))
		pp->formula = NULL;
	else
		pp->formula = xstrdup(tmp2);

	if (pp->numAlias > 0) {
		for (i = 0; i < pp->numAlias; i++) {
			if (fgets(tmp2, sizeof tmp2, fp) == NULL) {
				warnx("%s: bad alias: feof %s", __func__, file);
				return;
			}

			if (!(len = strlen(tmp2))) {
				fprintf(stderr, "FICS: Error bad alias in "
				    "file %s\n", file);
				i--;
				pp->numAlias--;
			} else {
				tmp2[strcspn(tmp2, "\n")] = '\0';
				tmp = tmp2;
				tmp = eatword(tmp2);
				*tmp = '\0';
				tmp++;
				tmp = eatwhite(tmp);

				pp->alias_list[i].comm_name = xstrdup(tmp2);
				pp->alias_list[i].alias = xstrdup(tmp);
			}
		}
	}

	if (add_to_list(fp, L_CENSOR, &size_cens, p) == -1) {
		warnx("%s: add to list error (L_CENSOR): player: %s",
		    __func__, parray[p].name);
	} else if (add_to_list(fp, L_NOTIFY, &size_not, p) == -1) {
		warnx("%s: add to list error (L_NOTIFY): player: %s",
		    __func__, parray[p].name);
	} else if (add_to_list(fp, L_NOPLAY, &size_noplay, p) == -1) {
		warnx("%s: add to list error (L_NOPLAY): player: %s",
		    __func__, parray[p].name);
	} else if (add_to_list(fp, L_GNOTIFY, &size_gnot, p) == -1) {
		warnx("%s: add to list error (L_GNOTIFY): player: %s",
		    __func__, parray[p].name);
	} else if (add_to_list(fp, L_CHANNEL, &size_chan, p) == -1) {
		warnx("%s: add to list error (L_CHANNEL): player: %s",
		    __func__, parray[p].name);
	}
}

PRIVATE int
got_attr_value_player(int p, char *attr, char *value, FILE *fp, char *file)
{
	char	*tmp1;
	char	 tmp[MAX_LINE_SIZE] = { '\0' };
	int	 i, len;

	if (!strcmp(attr, "name:")) {
		parray[p].name = xstrdup(value);
	} else if (!strcmp(attr, "password:")) {
		parray[p].passwd = xstrdup(value);
	} else if (!strcmp(attr, "fullname:")) {
		parray[p].fullName = xstrdup(value);
	} else if (!strcmp(attr, "email:")) {
		parray[p].emailAddress = xstrdup(value);
	} else if (!strcmp(attr, "prompt:")) {
		parray[p].prompt = xstrdup(value);
	} else if (!strcmp(attr, "s_num:")) {
		parray[p].s_stats.num = atoi(value);
	} else if (!strcmp(attr, "s_win:")) {
		parray[p].s_stats.win = atoi(value);
	} else if (!strcmp(attr, "s_loss:")) {
		parray[p].s_stats.los = atoi(value);
	} else if (!strcmp(attr, "s_draw:")) {
		parray[p].s_stats.dra = atoi(value);
	} else if (!strcmp(attr, "s_rating:")) {
		parray[p].s_stats.rating = atoi(value);
	} else if (!strcmp(attr, "s_sterr:")) {
		parray[p].s_stats.sterr = (atoi(value) / 10.0);
	} else if (!strcmp(attr, "s_ltime:")) {
		parray[p].s_stats.ltime = atoi(value);
	} else if (!strcmp(attr, "s_best:")) {
		parray[p].s_stats.best = atoi(value);
	} else if (!strcmp(attr, "s_wbest:")) {
		parray[p].s_stats.whenbest = atoi(value);
	} else if (!strcmp(attr, "b_num:")) {
		parray[p].b_stats.num = atoi(value);
	} else if (!strcmp(attr, "b_win:")) {
		parray[p].b_stats.win = atoi(value);
	} else if (!strcmp(attr, "b_loss:")) {
		parray[p].b_stats.los = atoi(value);
	} else if (!strcmp(attr, "b_draw:")) {
		parray[p].b_stats.dra = atoi(value);
	} else if (!strcmp(attr, "b_rating:")) {
		parray[p].b_stats.rating = atoi(value);
	} else if (!strcmp(attr, "b_sterr:")) {
		parray[p].b_stats.sterr = (atoi(value) / 10.0);
	} else if (!strcmp(attr, "b_ltime:")) {
		parray[p].b_stats.ltime = atoi(value);
	} else if (!strcmp(attr, "b_best:")) {
		parray[p].b_stats.best = atoi(value);
	} else if (!strcmp(attr, "b_wbest:")) {
		parray[p].b_stats.whenbest = atoi(value);
	} else if (!strcmp(attr, "w_num:")) {
		parray[p].w_stats.num = atoi(value);
	} else if (!strcmp(attr, "w_win:")) {
		parray[p].w_stats.win = atoi(value);
	} else if (!strcmp(attr, "w_loss:")) {
		parray[p].w_stats.los = atoi(value);
	} else if (!strcmp(attr, "w_draw:")) {
		parray[p].w_stats.dra = atoi(value);
	} else if (!strcmp(attr, "w_rating:")) {
		parray[p].w_stats.rating = atoi(value);
	} else if (!strcmp(attr, "w_sterr:")) {
		parray[p].w_stats.sterr = (atoi(value) / 10.0);
	} else if (!strcmp(attr, "w_ltime:")) {
		parray[p].w_stats.ltime = atoi(value);
	} else if (!strcmp(attr, "w_best:")) {
		parray[p].w_stats.best = atoi(value);
	} else if (!strcmp(attr, "w_wbest:")) {
		parray[p].w_stats.whenbest = atoi(value);
	} else if (!strcmp(attr, "open:")) {
		parray[p].open = atoi(value);
	} else if (!strcmp(attr, "rated:")) {
		parray[p].rated = atoi(value);
	} else if (!strcmp(attr, "ropen:")) {
		parray[p].ropen = atoi(value);
	} else if (!strcmp(attr, "bell:")) {
		parray[p].bell = atoi(value);
	} else if (!strcmp(attr, "pgn:")) {
		parray[p].pgn = atoi(value);
	} else if (!strcmp(attr, "timeofreg:")) {
		parray[p].timeOfReg = atoi(value);
	} else if (!strcmp(attr, "totaltime:")) {
		parray[p].totalTime = atoi(value);
	} else if (!strcmp(attr, "notifiedby:")) {
		parray[p].notifiedby = atoi(value);
	} else if (!strcmp(attr, "i_login:")) {
		parray[p].i_login = atoi(value);
	} else if (!strcmp(attr, "i_game:")) {
		parray[p].i_game = atoi(value);
	} else if (!strcmp(attr, "i_shout:")) {
		parray[p].i_shout = atoi(value);
	} else if (!strcmp(attr, "i_cshout:")) {
		parray[p].i_cshout = atoi(value);
	} else if (!strcmp(attr, "i_tell:")) {
		parray[p].i_tell = atoi(value);
	} else if (!strcmp(attr, "i_kibitz:")) {
		parray[p].i_kibitz = atoi(value);
	} else if (!strcmp(attr, "kiblevel:")) {
		parray[p].kiblevel = atoi(value);
	} else if (!strcmp(attr, "private:")) {
		parray[p].private = atoi(value);
	} else if (!strcmp(attr, "jprivate:")) {
		parray[p].jprivate = atoi(value);
	} else if (!strcmp(attr, "automail:")) {
		parray[p].automail = atoi(value);
	} else if (!strcmp(attr, "i_mailmess:")) {
		parray[p].i_mailmess = atoi(value);
	} else if (!strcmp(attr, "style:")) {
		parray[p].style = atoi(value);
	} else if (!strcmp(attr, "d_time:")) {
		parray[p].d_time = atoi(value);
	} else if (!strcmp(attr, "d_inc:")) {
		parray[p].d_inc = atoi(value);
	} else if (!strcmp(attr, "d_height:")) {
		parray[p].d_height = atoi(value);
	} else if (!strcmp(attr, "d_width:")) {
		parray[p].d_width = atoi(value);
	} else if (!strcmp(attr, "language:")) {
		parray[p].language = atoi(value);
	} else if (!strcmp(attr, "admin_level:")) {
		parray[p].adminLevel = atoi(value);

		if (parray[p].adminLevel >= ADMIN_ADMIN)
			parray[p].i_admin = 1;
	} else if (!strcmp(attr, "i_admin:")) {
		/* parray[p].i_admin = atoi(value) */;
	} else if (!strcmp(attr, "computer:")) {
		/* parray[p].computer = atoi(value) */;
	} else if (!strcmp(attr, "black_games:")) {
		parray[p].num_black = atoi(value);
	} else if (!strcmp(attr, "white_games:")) {
		parray[p].num_white = atoi(value);
	} else if (!strcmp(attr, "uscf:")) {
		/* parray[p].uscfRating = atoi(value) */;
	} else if (!strcmp(attr, "muzzled:")) {
		/* obsolete */;
	} else if (!strcmp(attr, "cmuzzled:")) {
		/* obsolete */;
	} else if (!strcmp(attr, "highlight:")) {
		parray[p].highlight = atoi(value);
	} else if (!strcmp(attr, "network:")) {
		/* parray[p].network_player = atoi(value) */;
	} else if (!strcmp(attr, "lasthost:")) {
		parray[p].lastHost = atoi(value);
	} else if (!strcmp(attr, "channel:")) {
		list_addsub(p, "channel", value, 1);
	} else if (!strcmp(attr, "num_comments:")) {
		parray[p].num_comments = atoi(value);
	} else if (!strcmp(attr, "num_plan:")) {
		/*
		 * num_plan
		 */

		parray[p].num_plan = atoi(value);

		if (parray[p].num_plan > 0) {
			for (i = 0; i < parray[p].num_plan; i++) {

				if (fgets(tmp, sizeof tmp, fp) == NULL) {
					warnx("%s: bad plan: feof %s",
					    __func__, file);
					return -1;
				}

				if (!(len = strlen(tmp))) {
					fprintf(stderr, "FICS: Error bad plan "
					    "in file %s\n", file);
					i--;
					parray[p].num_plan--;
				} else {
					// Get rid of '\n'.
					tmp[strcspn(tmp, "\n")] = '\0';

					parray[p].planLines[i] = (len > 1 ?
					    xstrdup(tmp) : NULL);
				}
			}
		}
	} else if (!strcmp(attr, "num_formula:")) {
		/*
		 * num_formula
		 */

		parray[p].num_formula = atoi(value);

		if (parray[p].num_formula > 0) {
			for (i = 0; i < parray[p].num_formula; i++) {
				if (fgets(tmp, sizeof tmp, fp) == NULL) {
					warnx("%s: bad formula: feof %s",
					    __func__, file);
					return -1;
				}

				if (!(len = strlen(tmp))) {
					fprintf(stderr, "FICS: Error bad "
					    "formula in file %s\n", file);
					i--;
					parray[p].num_formula--;
				} else {
					// Get rid of '\n'.
					tmp[strcspn(tmp, "\n")] = '\0';

					parray[p].formulaLines[i] = (len > 1 ?
					    xstrdup(tmp) : NULL);
				}
			}
		}
	} else if (!strcmp(attr, "formula:")) {
		/*
		 * formula
		 */

		parray[p].formula = xstrdup(value);
	} else if (!strcmp(attr, "num_alias:")) {
		/*
		 * num_alias
		 */

		parray[p].numAlias = atoi(value);

		if (parray[p].numAlias > 0) {
			for (i = 0; i < parray[p].numAlias; i++) {
				if (fgets(tmp, sizeof tmp, fp) == NULL) {
					warnx("%s: bad alias: feof %s",
					    __func__, file);
					return -1;
				}

				if (!(len = strlen(tmp))) {
					fprintf(stderr, "FICS: Error bad alias "
					    "in file %s\n", file);
					i--;
					parray[p].numAlias--;
				} else {
					tmp[strcspn(tmp, "\n")] = '\0';
					tmp1 = tmp;
					tmp1 = eatword(tmp1);
					*tmp1 = '\0';
					tmp1++;
					tmp1 = eatwhite(tmp1);

					parray[p].alias_list[i].comm_name =
					    xstrdup(tmp);
					parray[p].alias_list[i].alias =
					    xstrdup(tmp1);
				}
			}
		}
	} else if (!strcmp(attr, "num_censor:")) {
		/*
		 * num_censor
		 */

		i = atoi(value);

		while (i--) {
			if (fgets(tmp, sizeof tmp, fp) == NULL) {
				warnx("%s: bad censor: feof %s",
				    __func__, file);
				return -1;
			}

			if (!(len = strlen(tmp)) || len == 1) {	// blank lines
								// do occur!
				fprintf(stderr, "FICS: Error bad censor in "
				    "file %s\n", file);
			} else {
				tmp[strcspn(tmp, "\n")] = '\0';
				list_add(p, L_CENSOR, tmp);
			}
		}
	} else if (!strcmp(attr, "num_notify:")) {
		i = atoi(value);

		while (i--) {
			if (fgets(tmp, sizeof tmp, fp) == NULL) {
				warnx("%s: bad notify: feof %s",
				    __func__, file);
				return -1;
			}

			if (!(len = strlen(tmp)) || len == 1) {	// blank lines
								// do occur!
				fprintf(stderr, "FICS: Error bad notify in "
				    "file %s\n", file);
			} else {
				tmp[strcspn(tmp, "\n")] = '\0';
				list_add(p, L_NOTIFY, tmp);
			}
		}
	} else if (!strcmp(attr, "num_noplay:")) {
		i = atoi(value);

		while (i--) {
			if (fgets(tmp, sizeof tmp, fp) == NULL) {
				warnx("%s: bad noplay: feof %s",
				    __func__, file);
				return -1;
			}

			if (!(len = strlen(tmp)) || len == 1) {	// blank lines
								// do occur!
				fprintf(stderr, "FICS: Error bad noplay in "
				    "file %s\n", file);
			} else {
				tmp[strcspn(tmp, "\n")] = '\0';
				list_add(p, L_NOPLAY, tmp);
			}
		}
	} else if (!strcmp(attr, "num_gnotify:")) {
		i = atoi(value);

		while (i--) {
			if (fgets(tmp, sizeof tmp, fp) == NULL) {
				warnx("%s: bad gnotify: feof %s",
				    __func__, file);
				return -1;
			}

			if (!(len = strlen(tmp)) || len == 1) {	// blank lines
								// do occur!
				fprintf(stderr, "FICS: Error bad gnotify in "
				    "file %s\n", file);
			} else {
				tmp[strcspn(tmp, "\n")] = '\0';
				list_add(p, L_GNOTIFY, tmp);
			}
		}
	} else {
		fprintf(stderr, "FICS: Error bad attribute >%s< from file %s\n",
		    attr, file);
	}

	return 0;
}

PUBLIC int
player_read(int p, char *name)
{
	FILE	*fp = NULL;
	char	*attr, *value;
	char	 fname[MAX_FILENAME_SIZE] = { '\0' };
	char	 line[MAX_LINE_SIZE] = { '\0' };
	int	 len = 0;
	int	 version = 0;

	parray[p].login = stolower(xstrdup(name));

	snprintf(fname, sizeof fname, "%s/%c/%s", player_dir,
	    parray[p].login[0], parray[p].login);

	if ((fp = fopen(fname, "r")) == NULL) { // Unregistered player
		parray[p].name = xstrdup(name);
		parray[p].registered = 0;
		return -1;
	}

	parray[p].registered = 1; // Lets load the file

	if (fgets(line, sizeof line, fp) == NULL) {	// Ok, so which version
		warnx("%s: fgets() error", __func__);	// file?
		fclose(fp);
		return -1;
	}

	if (line[0] == 'v')
		sscanf(line, "%*c %d", &version);
	if (version > 0)	// Quick method:
		ReadV1PlayerFmt(p, &parray[p], fp, fname, version);
	else {			// Do it the old SLOW way
		do {
			if (feof(fp))
				break;
			if ((len = strlen(line)) <= 1)
				continue;

			line[len - 1] = '\0';
			attr = eatwhite(line);

			if (attr[0] == '#')
				continue; // Comment

			value = eatword(attr);

			if (!*value) {
				fprintf(stderr, "FICS: Error reading file %s\n",
				    fname);
				continue;
			}

			*value = '\0';
			value++;
			value = eatwhite(value);
			stolower(attr);
			got_attr_value_player(p, attr, value, fp, fname);

			if (fgets(line, sizeof line, fp) == NULL)
				break;
		} while (!feof(fp));
	}

	fclose(fp);

	if (version == 0) {
		player_save(p);	// Ensure old files are quickly converted e.g.
				// when someone fingers.
	}

	if (!parray[p].name) {
		parray[p].name = xstrdup(name);
		pprintf(p, "\n*** WARNING: Your Data file is corrupt. "
		    "Please tell an admin ***\n");
	}

	return 0;
}

PUBLIC int
player_delete(int p)
{
	char fname[MAX_FILENAME_SIZE];

	if (!parray[p].registered)	// Player must not be registered
		return -1;

	snprintf(fname, sizeof fname, "%s/%c/%s", player_dir,
	    parray[p].login[0], parray[p].login);
	unlink(fname);

	return 0;
}

PUBLIC int
player_markdeleted(int p)
{
	FILE	*fp;
	char	 fname[MAX_FILENAME_SIZE];
	char	 fname2[MAX_FILENAME_SIZE];

	if (!parray[p].registered)	// Player must not be registered
		return -1;

	snprintf(fname, sizeof fname, "%s/%c/%s", player_dir,
	    parray[p].login[0], parray[p].login);
	snprintf(fname2, sizeof fname2, "%s/%c/%s.delete", player_dir,
	    parray[p].login[0], parray[p].login);
	rename(fname, fname2);

	if ((fp = fopen(fname2, "a")) != NULL) { // Touch the file
		fprintf(fp, "\n");
		fclose(fp);
	}

	return 0;
}

PRIVATE void
WritePlayerFile(FILE *fp, int p)
{
	int	 i;
	player	*pp = &parray[p];

	fprintf(fp, "v %d\n", PLAYER_VERSION);

	fprintf(fp, "%s\n", (pp->name ? pp->name : "NONE"));
	fprintf(fp, "%s\n", (pp->fullName ? pp->fullName : "NONE"));
	fprintf(fp, "%s\n", (pp->passwd ? pp->passwd : "NONE"));
	fprintf(fp, "%s\n", (pp->emailAddress ? pp->emailAddress : "NONE"));

	fprintf(fp, "%d %d %d %d %d %d %jd %d %jd %d %d %d %d %d %d %jd %d %jd %d "
	    "%d %d %d %d %d %jd %d %jd %d %d %d %d %d %d %jd %d %jd %d %d %d %d %d "
	    "%d %jd %d %jd %u\n",
	    pp->s_stats.num, pp->s_stats.win, pp->s_stats.los,
	    pp->s_stats.dra, pp->s_stats.rating,
	    (int)(pp->s_stats.sterr * 10.0),
	    (intmax_t)pp->s_stats.ltime, pp->s_stats.best,
	    (intmax_t)pp->s_stats.whenbest,

	    pp->b_stats.num, pp->b_stats.win, pp->b_stats.los,
	    pp->b_stats.dra, pp->b_stats.rating,
	    (int)(pp->b_stats.sterr * 10.0),
	    (intmax_t)pp->b_stats.ltime, pp->b_stats.best,
	    (intmax_t)pp->b_stats.whenbest,

	    pp->w_stats.num, pp->w_stats.win, pp->w_stats.los,
	    pp->w_stats.dra, pp->w_stats.rating,
	    (int)(pp->w_stats.sterr * 10.0),
	    (intmax_t)pp->w_stats.ltime, pp->w_stats.best,
	    (intmax_t)pp->w_stats.whenbest,

	    pp->l_stats.num, pp->l_stats.win, pp->l_stats.los,
	    pp->l_stats.dra, pp->l_stats.rating,
	    (int)(pp->l_stats.sterr * 10.0),
	    (intmax_t)pp->l_stats.ltime, pp->l_stats.best,
	    (intmax_t)pp->l_stats.whenbest,

	    pp->bug_stats.num, pp->bug_stats.win, pp->bug_stats.los,
	    pp->bug_stats.dra, pp->bug_stats.rating,
	    (int)(pp->bug_stats.sterr * 10.0),
	    (intmax_t)pp->bug_stats.ltime, pp->bug_stats.best,
	    (intmax_t)pp->bug_stats.whenbest,

	    pp->lastHost); /* fprintf() */

	fprintf(fp, "%s\n", pp->prompt);

	fprintf(fp, "%d %d %d %jd %jd %d %d %d %d %d %d %d %d %d %d %d %d %d %d "
	    "%d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d\n",
	    pp->open, pp->rated, pp->ropen,
	    (intmax_t)pp->timeOfReg,
	    (intmax_t)pp->totalTime,
	    pp->bell, pp->pgn, pp->notifiedby, pp->i_login, pp->i_game,
	    pp->i_shout, pp->i_cshout, pp->i_tell, pp->i_kibitz, pp->private,
	    pp->jprivate, pp->automail, pp->i_mailmess, pp->style, pp->d_time,
	    pp->d_inc, pp->d_height, pp->d_width, pp->language, pp->adminLevel,
	    pp->num_white, pp->num_black, pp->highlight, pp->num_comments,
	    pp->num_plan, pp->num_formula,

	    list_size(p, L_CENSOR),
	    list_size(p, L_NOTIFY),
	    list_size(p, L_NOPLAY),
	    list_size(p, L_GNOTIFY),
	    pp->numAlias,
	    list_size(p, L_CHANNEL));

	for (i = 0; i < pp->num_plan; i++)
		fprintf(fp, "%s\n", (pp->planLines[i] ? pp->planLines[i] : ""));
	for (i = 0; i < pp->num_formula; i++) {
		fprintf(fp, "%s\n", (pp->formulaLines[i] ? pp->formulaLines[i] :
		    ""));
	}

	if (parray[p].formula != NULL)
		fprintf(fp, "%s\n", pp->formula);
	else
		fprintf(fp, "NONE\n");

	for (i = 0; i < pp->numAlias; i++) {
		fprintf(fp, "%s %s\n", pp->alias_list[i].comm_name,
		    pp->alias_list[i].alias);
	}

	list_print(fp, p, L_CENSOR);
	list_print(fp, p, L_NOTIFY);
	list_print(fp, p, L_NOPLAY);
	list_print(fp, p, L_GNOTIFY);
	list_print(fp, p, L_CHANNEL);
}

PUBLIC int
player_save(int p)
{
	FILE	*fp;
	char	 fname[MAX_FILENAME_SIZE];

	if (!player_num_ok_chk(p)) {
		warnx("%s: invalid player number %d", __func__, p);
		return -1;
	}

	if (!parray[p].registered)	// Player must not be registered
		return -1;

	if (parray[p].name == NULL) {	// Fixes a bug if name is null
		pprintf(p, "WARNING: Your player file could not be updated, "
		    "due to corrupt data.\n");
		return -1;
	}

	if (strcasecmp(parray[p].login, parray[p].name)) {
		pprintf(p, "WARNING: Your player file could not be updated, "
		    "due to corrupt data.\n");
		return -1;
	}

	snprintf(fname, sizeof fname, "%s/%c/%s", player_dir,
	    parray[p].login[0], parray[p].login);

	if ((fp = fopen(fname, "w")) == NULL) {
		warn("%s: Problem opening file %s for write", __func__, fname);
		return -1;
	}

	WritePlayerFile(fp, p);
	fclose(fp);
	return 0;
}

PUBLIC int
player_find(int fd)
{
	for (int i = 0; i < p_num; i++) {
		if (parray[i].status == PLAYER_EMPTY)
			continue;
		if (parray[i].socket == fd)
			return i;
	}

	return -1;
}

PUBLIC int
player_find_bylogin(char *name)
{
	for (int i = 0; i < p_num; i++) {
		if (parray[i].status == PLAYER_EMPTY ||
		    parray[i].status == PLAYER_LOGIN ||
		    parray[i].status == PLAYER_PASSWORD)
			continue;

		if (!parray[i].login)
			continue;

		if (!strcmp(parray[i].login, name))
			return i;
	}

	return -1;
}

PUBLIC int
player_find_part_login(char *name)
{
	int	found = -1;
	int	i;

	if ((i = player_find_bylogin(name)) >= 0)
		return i;

	for (i = 0; i < p_num; i++) {
		if (parray[i].status == PLAYER_EMPTY ||
		    parray[i].status == PLAYER_LOGIN ||
		    parray[i].status == PLAYER_PASSWORD)
			continue;

		if (!parray[i].login)
			continue;

		if (!strncmp(parray[i].login, name, strlen(name))) {
			if (found >= 0) /* Ambiguous */
				return -2;
			found = i;
		}
	}

	return found;
}

PUBLIC int
player_censored(int p, int p1)
{
	if (in_list(p, L_CENSOR, parray[p1].login))
		return 1;
	return 0;
}

/*
 * Is p1 on p's notify list?
 */
PUBLIC int
player_notified(int p, int p1)
{
	if (!parray[p1].registered)
		return 0;

	/* Possible bug: 'p' has just arrived! */
	if (!parray[p].name)
		return 0;

	return in_list(p, L_NOTIFY, parray[p1].login);
}

PUBLIC void
player_notify_departure(int p)
{
	if (!parray[p].registered)
		return;

	for (int p1 = 0; p1 < p_num; p1++) {
		if (parray[p1].notifiedby &&
		    !player_notified(p1, p) &&
		    player_notified(p, p1) &&
		    parray[p1].status == PLAYER_PROMPT) {
			if (parray[p1].bell)
				pprintf_noformat(p1, "\007");
			pprintf(p1, "\nNotification: ");
			pprintf_highlight(p1, "%s", parray[p].name);
			pprintf_prompt(p1, " has departed and isn't on your "
			    "notify list.\n");
		}
	}
}

PUBLIC int
player_notify_present(int p)
{
	int	count = 0;
	int	p1;

	if (!parray[p].registered)
		return count;

	for (p1 = 0; p1 < p_num; p1++) {
		if (player_notified(p, p1) && parray[p1].status ==
		    PLAYER_PROMPT) {
			if (!count)
				pprintf(p, "Present company includes:");
			count++;

			pprintf(p, " %s", parray[p1].name);

			if (parray[p1].notifiedby &&
			    !player_notified(p1, p) &&
			    parray[p1].status == PLAYER_PROMPT) {
				if (parray[p1].bell)
					pprintf_noformat(p1, "\007");
				pprintf(p1, "\nNotification: ");
				pprintf_highlight(p1, "%s", parray[p].name);
				pprintf_prompt(p1, " has arrived and isn't on "
				    "your notify list.\n");
			}
		}
	}

	if (count)
		pprintf(p, ".\n");
	return count;
}

PUBLIC int
player_notify(int p, char *note1, char *note2)
{ // Notify those interested that 'p' has arrived/departed.
	int	count = 0;
	int	p1;

	if (!parray[p].registered)
		return count;

	for (p1 = 0; p1 < p_num; p1++) {
		if (player_notified(p1, p) && parray[p1].status ==
		    PLAYER_PROMPT) {
			if (parray[p1].bell)
				pprintf_noformat(p1, "\007");

			pprintf(p1, "\nNotification: ");
			pprintf_highlight(p1, "%s", parray[p].name);
			pprintf_prompt(p1, " has %s.\n", note1);

			if (!count)
				pprintf(p, "Your %s was noted by:", note2);
			count++;

			pprintf(p, " %s", parray[p1].name);
		}
	}

	if (count)
		pprintf(p, ".\n");
	return count;
}

/*
 * Show adjourned games upon logon
 */
PUBLIC int
showstored(int p)
{
	DIR		*dirp;
	char		 dname[MAX_FILENAME_SIZE];
	int		 c = 0, p1;
	multicol	*m = multicol_start(50);	// Limit to 50
							// (should be enough)
#ifdef USE_DIRENT
	struct dirent	*dp;
#else
	struct direct	*dp;
#endif

	snprintf(dname, sizeof dname, "%s/%c", adj_dir, parray[p].login[0]);

	if ((dirp = opendir(dname)) == NULL) {
		multicol_end(m);
		return COM_OK;
	}

	for (dp = readdir(dirp); dp != NULL; dp = readdir(dirp)) {
		if (file_has_pname(dp->d_name, parray[p].login)) {
			if (strcmp(file_wplayer(dp->d_name), parray[p].login)) {
				p1 = player_find_bylogin
				    (file_wplayer(dp->d_name));
			} else {
				p1 = player_find_bylogin
				    (file_bplayer(dp->d_name));
			}

			if (p1 >= 0) {
				if (c < 50)
					multicol_store(m, parray[p1].name);
				pprintf(p1, "\nNotification: ");
				pprintf_highlight(p1, "%s", parray[p].name);
				pprintf_prompt(p1, ", who has an adjourned "
				    "game with you, has arrived.\n");
				c++;
			}
		}
	}

	closedir(dirp);

	if (c == 1) {
		pprintf(p, "1 player, who has an adjourned game with you, is "
		    "online:\007");
	} else if (c > 1) {
		pprintf(p, "\n%d players, who have an adjourned game with you, "
		    "are online:\007", c);
	}

	if (c != 0)
		multicol_pprint(m, p, parray[p].d_width, 2);
	multicol_end(m);
	return COM_OK;
}

PUBLIC int
player_count(int CountAdmins)
{
	int	count;
	int	i;

	count = 0;
	i = 0;

	while (i < p_num) {
		if (parray[i].status == PLAYER_PROMPT &&
		    (CountAdmins || !in_list(i, L_ADMIN, parray[i].name)))
			count++;
		i++;
	}

	if (count > player_high)
		player_high = count;
	return count;
}

PUBLIC int
player_idle(int p)
{ // XXX
	if (parray[p].status != PLAYER_PROMPT)
		return (time(NULL) - parray[p].logon_time);

	return (time(NULL) - parray[p].last_command_time);
}

PUBLIC int
player_ontime(int p)
{ // XXX
	return (time(NULL) - parray[p].logon_time);
}

PRIVATE void
write_p_inout(int inout, int p, char *file, int maxlines)
{
	FILE *fp;

	if ((fp = fopen(file, "a")) == NULL)
		return;

	fprintf(fp, "%d %s %ld %d %s\n", inout, parray[p].name,
	    (long int)time(NULL), parray[p].registered,
	    dotQuad(parray[p].thisHost));

	fclose(fp);

	if (maxlines)
		truncate_file(file, maxlines);
}

PUBLIC void
player_write_login(int p)
{
	char fname[MAX_FILENAME_SIZE];

	if (parray[p].registered) {
		snprintf(fname, sizeof fname, "%s/player_data/%c/%s.%s",
		    stats_dir, parray[p].login[0], parray[p].login,
		    STATS_LOGONS);
		write_p_inout(P_LOGIN, p, fname, 8);
	}

	snprintf(fname, sizeof fname, "%s/%s", stats_dir, STATS_LOGONS);
	write_p_inout(P_LOGIN, p, fname, 30);

	snprintf(fname, sizeof fname, "%s/%s", stats_dir, "logons.log");
	write_p_inout(P_LOGIN, p, fname, 0);
}

PUBLIC void
player_write_logout(int p)
{
	char fname[MAX_FILENAME_SIZE];

	if (parray[p].registered) {
		snprintf(fname, sizeof fname, "%s/player_data/%c/%s.%s",
		    stats_dir, parray[p].login[0], parray[p].login,
		    STATS_LOGONS);
		write_p_inout(P_LOGOUT, p, fname, 8);
	}

	snprintf(fname, sizeof fname, "%s/%s", stats_dir, STATS_LOGONS);
	write_p_inout(P_LOGOUT, p, fname, 30);

	snprintf(fname, sizeof fname, "%s/%s", stats_dir, "logons.log");
	write_p_inout(P_LOGOUT, p, fname, 0);
}

PUBLIC time_t
player_lastconnect(int p)
{
	FILE		*fp;
	char		 fname[MAX_FILENAME_SIZE];
	char		 ipstr[20];
	char		 loginName[MAX_LOGIN_NAME];
	int		 inout, registered;
	int		 ret, too_long;
	long int	 lval;
	time_t		 last = 0;

	ret = snprintf(fname, sizeof fname, "%s/player_data/%c/%s.%s",
	    stats_dir, parray[p].login[0], parray[p].login, STATS_LOGONS);
	too_long = (ret < 0 || (size_t)ret >= sizeof fname);

	if (too_long) {
		fprintf(stderr, "FICS: %s: warning: snprintf truncated\n",
		    __func__);
	}

	if ((fp = fopen(fname, "r")) == NULL)
		return 0;

	inout = 1;

	while (!feof(fp)) {
		if (inout == P_LOGIN)
			last = lval;

		_Static_assert(19 < ARRAY_SIZE(loginName),
		    "'loginName' too small");
		_Static_assert(19 < ARRAY_SIZE(ipstr),
		    "'ipstr' too small");

		if (fscanf(fp, "%d %19s %ld %d %19s\n", &inout, loginName,
		    &lval, &registered, ipstr) != 5) {
			fprintf(stderr, "FICS: Error in login info format. %s"
			    "\n", fname);
			fclose(fp);
			return 0;
		}
	}

	fclose(fp);
	return last;
}

PUBLIC time_t
player_lastdisconnect(int p)
{
	FILE		*fp;
	char		 fname[MAX_FILENAME_SIZE];
	char		 ipstr[20];
	char		 loginName[MAX_LOGIN_NAME];
	int		 inout, registered;
	int		 ret, too_long;
	long int	 lval;
	time_t		 last = 0;

	ret = snprintf(fname, sizeof fname, "%s/player_data/%c/%s.%s",
	    stats_dir, parray[p].login[0], parray[p].login, STATS_LOGONS);
	too_long = (ret < 0 || (size_t)ret >= sizeof fname);

	if (too_long) {
		fprintf(stderr, "FICS: %s: warning: snprintf truncated\n",
		    __func__);
	}

	if ((fp = fopen(fname, "r")) == NULL)
		return 0;

	while (!feof(fp)) {
		_Static_assert(19 < ARRAY_SIZE(loginName),
		    "'loginName' too small");
		_Static_assert(19 < ARRAY_SIZE(ipstr),
		    "'ipstr' too small");

		if (fscanf(fp, "%d %19s %ld %d %19s\n", &inout, loginName,
		    &lval, &registered, ipstr) != 5) {
			fprintf(stderr, "FICS: Error in login info format. %s"
			    "\n", fname);
			fclose(fp);
			return 0;
		}

		if (inout == P_LOGOUT)
			last = lval;
	}

	fclose(fp);
	return last;
}

PUBLIC void
player_pend_print(int p, pending *pend)
{
	char	outstr[200] = { '\0' };
	char	tmp[200] = { '\0' };

	if (p == pend->whofrom) {
		(void)strlcpy(outstr, "You are offering ", sizeof outstr);
	} else {
		snprintf(outstr, sizeof outstr, "%s is offering ",
		    parray[pend->whofrom].name);
	}

	if (p == pend->whoto) {
		/* null */;
	} else {
		snprintf(tmp, sizeof tmp, "%s ", parray[pend->whoto].name);
	}

	if (strlcat(outstr, tmp, sizeof outstr) >= sizeof outstr) {
		fprintf(stderr, "FICS: %s: warning: strlcat() truncated\n",
		    __func__);
	}

	switch (pend->type) {
	case PEND_MATCH:
		snprintf(tmp, sizeof tmp, "%s.", game_str(pend->param5,
		    (pend->param1 * 60),
		    pend->param2,
		    (pend->param3 * 60),
		    pend->param4,
		    pend->char1,
		    pend->char2));
		break;
	case PEND_DRAW:
		strlcpy(tmp, "a draw.\n", sizeof tmp);
		break;
	case PEND_PAUSE:
		strlcpy(tmp, "to pause the clock.\n", sizeof tmp);
		break;
	case PEND_ABORT:
		strlcpy(tmp, "to abort the game.\n", sizeof tmp);
		break;
	case PEND_TAKEBACK:
		snprintf(tmp, sizeof tmp, "to takeback the last %d "
		    "half moves.\n", pend->param1);
		break;
	case PEND_SIMUL:
		strlcpy(tmp, "to play a simul match.\n", sizeof tmp);
		break;
	case PEND_SWITCH:
		strlcpy(tmp, "to switch sides.\n", sizeof tmp);
		break;
	case PEND_ADJOURN:
		strlcpy(tmp, "an adjournment.\n", sizeof tmp);
		break;
	case PEND_PARTNER:
		strlcpy(tmp, "to be bughouse partners.\n", sizeof tmp);
		break;
	}

	if (strlcat(outstr, tmp, sizeof outstr) >= sizeof outstr) {
		fprintf(stderr, "FICS: %s: warning: strlcat() truncated\n",
		    __func__);
	}

	pprintf(p, "%s\n", outstr);
}

PUBLIC int
player_find_pendto(int p, int p1, int type)
{
	for (int i = 0; i < parray[p].num_to; i++) {
		if (parray[p].p_to_list[i].whoto != p1 && p1 != -1)
			continue;
		if (type < 0 || parray[p].p_to_list[i].type == type)
			return i;
		if (type == PEND_BUGHOUSE &&
		    parray[p].p_to_list[i].type == PEND_MATCH &&
		    !strcmp(parray[p].p_to_list[i].char2, "bughouse"))
			return i;
	}

	return -1;
}

PUBLIC int
player_new_pendto(int p)
{
	if (parray[p].num_to >= MAX_PENDING)
		return -1;

	parray[p].num_to++;

	return (parray[p].num_to - 1);
}

PUBLIC int
player_remove_pendto(int p, int p1, int type)
{
	int w;

	if ((w = player_find_pendto(p, p1, type)) < 0)
		return -1;

	for (; w < (parray[p].num_to - 1); w++)
		parray[p].p_to_list[w] = parray[p].p_to_list[w + 1];

	parray[p].num_to = (parray[p].num_to - 1);

	return 0;
}

PUBLIC int
player_find_pendfrom(int p, int p1, int type)
{
	for (int i = 0; i < parray[p].num_from; i++) {
		if (parray[p].p_from_list[i].whofrom != p1 && p1 != -1)
			continue;
		if (type == PEND_ALL || parray[p].p_from_list[i].type == type)
			return i;
		if (type < 0 && parray[p].p_from_list[i].type != -type)
			return i;
		/*
		 * The above "if" allows a type of -PEND_SIMUL to
		 * match every request EXCEPT simuls, for example. I'm
		 * doing this because Heringer does not want to
		 * decline simul requests when he makes a move in a
		 * sumul. -- hersco.
		 */

		if (type == PEND_BUGHOUSE &&
		    parray[p].p_from_list[i].type == PEND_MATCH &&
		    !strcmp(parray[p].p_from_list[i].char2, "bughouse"))
			return i;
	}

	return -1;
}

PUBLIC int
player_new_pendfrom(int p)
{
	if (parray[p].num_from >= MAX_PENDING)
		return -1;

	parray[p].num_from++;

	return (parray[p].num_from - 1);
}

PUBLIC int
player_remove_pendfrom(int p, int p1, int type)
{
	int w;

	if ((w = player_find_pendfrom(p, p1, type)) < 0)
		return -1;

	for (; w < (parray[p].num_from - 1); w++)
		parray[p].p_from_list[w] = parray[p].p_from_list[w + 1];

	parray[p].num_from = (parray[p].num_from - 1);

	return 0;
}

PUBLIC int
player_add_request(int p, int p1, int type, int param)
{
	int	pendf;
	int	pendt;

	if (player_find_pendto(p, p1, type) >= 0)
		return -1; // Already exists

	if ((pendt = player_new_pendto(p)) == -1)
		return -1;
	if ((pendf = player_new_pendfrom(p1)) == -1) {
		parray[p].num_to--;
		return -1;
	}

	parray[p].p_to_list[pendt].type		= type;
	parray[p].p_to_list[pendt].whoto	= p1;
	parray[p].p_to_list[pendt].whofrom	= p;
	parray[p].p_to_list[pendt].param1	= param;

	parray[p1].p_from_list[pendf].type	= type;
	parray[p1].p_from_list[pendf].whoto	= p1;
	parray[p1].p_from_list[pendf].whofrom	= p;
	parray[p1].p_from_list[pendf].param1	= param;

	return 0;
}

PUBLIC int
player_remove_request(int p, int p1, int type)
{
	int to = 0, from = 0;

	while (to != -1) {
		if ((to = player_find_pendto(p, p1, type)) != -1) {
			for (; to < parray[p].num_to - 1; to++) {
				parray[p].p_to_list[to] =
				    parray[p].p_to_list[to + 1];
			}

			parray[p].num_to = (parray[p].num_to - 1);
		}
	}

	while (from != -1) {
		if ((from = player_find_pendfrom(p1, p, type)) != -1) {
			for (; from < parray[p1].num_from - 1; from++) {
				parray[p1].p_from_list[from] =
				    parray[p1].p_from_list[from + 1];
			}

			parray[p1].num_from = (parray[p1].num_from - 1);
		}
	}

	if ((type == PEND_ALL || type == PEND_MATCH) && parray[p].partner >= 0)
		player_remove_request(parray[p].partner, p1, PEND_BUGHOUSE);

	return 0;
}

PUBLIC int
player_decline_offers(int p, int p1, int offerType)
{
	char	*pName = parray[p].name;
	char	*p2Name;
	int	 count = 0;
	int	 offer;
	int	 part, p2part;
	int	 type, p2;

	// First get rid of bughouse offers from partner.

	if ((offerType == PEND_MATCH || offerType == PEND_ALL) &&
	    parray[p].partner >= 0 &&
	    parray[parray[p].partner].partner == p) {
		count += player_decline_offers(parray[p].partner, p1,
		    PEND_BUGHOUSE);
	}

	while ((offer = player_find_pendfrom(p, p1, offerType)) >= 0) {
		type	= parray[p].p_from_list[offer].type;
		p2	= parray[p].p_from_list[offer].whofrom;
		p2Name	= parray[p2].name;
		part	= parray[p].partner;

		if (part >= 0 && parray[part].partner != p)
			part = -1;

		p2part = parray[p2].partner;

		if (p2part >= 0 && parray[p2part].partner != p2)
			p2part = -1;

		switch (type) {
		case PEND_MATCH:
			pprintf_prompt(p2, "\n%s declines the match offer.\n",
			    pName);
			pprintf(p, "You decline the match offer from %s.\n",
			    p2Name);

			if (!strcmp(parray[p].p_from_list[offer].char2,
			    "bughouse")) {
				if (part >= 0) {
					pprintf_prompt(part, "Your partner "
					    "declines the bughouse offer from "
					    "%s.\n", parray[p2].name);
				}

				if (p2part >= 0) {
					pprintf_prompt(p2part, "%s declines "
					    "the bughouse offer from your "
					    "partner.\n", parray[p].name);
				}
			}

			break;
		case PEND_DRAW:
			pprintf_prompt(p2, "\n%s declines draw request.\n",
			    pName);
			pprintf(p, "You decline the draw request from %s.\n",
			    p2Name);
			break;
		case PEND_PAUSE:
			pprintf_prompt(p2, "\n%s declines pause request.\n",
			    pName);
			pprintf(p, "You decline the pause request from %s.\n",
			    p2Name);
			break;
		case PEND_ABORT:
			pprintf_prompt(p2, "\n%s declines abort request.\n",
			    pName);
			pprintf(p, "You decline the abort request from %s.\n",
			    p2Name);
			break;
		case PEND_TAKEBACK:
			pprintf_prompt(p2, "\n%s declines the takeback request."
			    "\n", pName);
			pprintf(p, "You decline the takeback request from %s."
			    "\n", p2Name);
			break;
		case PEND_ADJOURN:
			pprintf_prompt(p2, "\n%s declines the adjourn request."
			    "\n", pName);
			pprintf(p, "You decline the adjourn request from %s.\n",
			    p2Name);
			break;
		case PEND_SWITCH:
			pprintf_prompt(p2, "\n%s declines the switch sides "
			    "request.\n", pName);
			pprintf(p, "You decline the switch sides request from "
			    "%s.\n", p2Name);
			break;
		case PEND_SIMUL:
			pprintf_prompt(p2, "\n%s declines the simul offer.\n",
			    pName);
			pprintf(p, "You decline the simul offer from %s.\n",
			    p2Name);
			break;
		case PEND_PARTNER:
			pprintf_prompt(p2, "\n%s declines your partnership "
			    "request.\n", pName);
			pprintf(p, "You decline the partnership request from "
			    "%s.\n", p2Name);
			break;
		} /* switch */

		player_remove_request(p2, p, type);
		count++;
	} /* while */

	return count;
}

PUBLIC int
player_withdraw_offers(int p, int p1, int offerType)
{
	char	*pName = parray[p].name;
	char	*p2Name;
	int	 count = 0;
	int	 offer;
	int	 part, p2part;
	int	 type, p2;

	// First get rid of bughouse offers from partner.

	if ((offerType == PEND_MATCH || offerType == PEND_ALL) &&
	    parray[p].partner >= 0 &&
	    parray[parray[p].partner].partner == p) {
		count += player_withdraw_offers(parray[p].partner, p1,
		    PEND_BUGHOUSE);
	}

	while ((offer = player_find_pendto(p, p1, offerType)) >= 0) {
		type	= parray[p].p_to_list[offer].type;
		p2	= parray[p].p_to_list[offer].whoto;
		p2Name	= parray[p2].name;
		part	= parray[p].partner;

		if (part >= 0 && parray[part].partner != p)
			part = -1;

		p2part = parray[p2].partner;

		if (p2part >= 0 && parray[p2part].partner != p2)
			p2part = -1;

		switch (type) {
		case PEND_MATCH:
			pprintf_prompt(p2, "\n%s withdraws the match offer.\n",
			    pName);
			pprintf(p, "You withdraw the match offer to %s.\n",
			    p2Name);

			if (!strcmp(parray[p].p_to_list[offer].char2,
			    "bughouse")) {
				if (part >= 0) {
					pprintf_prompt(part, "Your partner "
					    "withdraws the bughouse offer to "
					    "%s.\n", parray[p2].name);
				}
				if (p2part >= 0) {
					pprintf_prompt(p2part, "%s withdraws "
					    "the bughouse offer to your "
					    "partner.\n", parray[p].name);
				}
			}

			break;
		case PEND_DRAW:
			pprintf_prompt(p2, "\n%s withdraws draw request.\n",
			    pName);
			pprintf(p, "You withdraw the draw request to %s.\n",
			    p2Name);
			break;
		case PEND_PAUSE:
			pprintf_prompt(p2, "\n%s withdraws pause request.\n",
			    pName);
			pprintf(p, "You withdraw the pause request to %s.\n",
			    p2Name);
			break;
		case PEND_ABORT:
			pprintf_prompt(p2, "\n%s withdraws abort request.\n",
			    pName);
			pprintf(p, "You withdraw the abort request to %s.\n",
			    p2Name);
			break;
		case PEND_TAKEBACK:
			pprintf_prompt(p2, "\n%s withdraws the takeback "
			    "request.\n", pName);
			pprintf(p, "You withdraw the takeback request to %s.\n",
			    p2Name);
			break;
		case PEND_ADJOURN:
			pprintf_prompt(p2, "\n%s withdraws the adjourn request."
			    "\n", pName);
			pprintf(p, "You withdraw the adjourn request to %s.\n",
			    p2Name);
			break;
		case PEND_SWITCH:
			pprintf_prompt(p2, "\n%s withdraws the switch sides "
			    "request.\n", pName);
			pprintf(p, "You withdraw the switch sides request to "
			    "%s.\n", p2Name);
			break;
		case PEND_SIMUL:
			pprintf_prompt(p2, "\n%s withdraws the simul offer.\n",
			    pName);
			pprintf(p, "You withdraw the simul offer to %s.\n",
			    p2Name);
			break;
		case PEND_PARTNER:
			pprintf_prompt(p2, "\n%s withdraws partnership request."
			    "\n", pName);
			pprintf(p, "You withdraw the partnership request to %s."
			    "\n", p2Name);
			break;
		} /* switch */

		player_remove_request(p, p2, type);
		count++;
	} /* while */

	return count;
}

PUBLIC int
player_is_observe(int p, int g)
{
	int i;

	for (i = 0; i < parray[p].num_observe; i++) {
		if (parray[p].observe_list[i] == g)
			break;
	}
	if (i == parray[p].num_observe)
		return 0;
	else
		return 1;
}

PUBLIC int
player_add_observe(int p, int g)
{
	if (parray[p].num_observe == MAX_OBSERVE)
		return -1;
	parray[p].observe_list[parray[p].num_observe] = g;
	parray[p].num_observe++;
	return 0;
}

PUBLIC int
player_remove_observe(int p, int g)
{
	int i;

	for (i = 0; i < parray[p].num_observe; i++) {
		if (parray[p].observe_list[i] == g)
			break;
	}

	if (i == parray[p].num_observe)
		return -1; // Not found!

	for (; i < (parray[p].num_observe - 1); i++)
		parray[p].observe_list[i] = parray[p].observe_list[i + 1];

	parray[p].num_observe--;

	return 0;
}

PUBLIC int
player_game_ended(int g)
{
	for (int p = 0; p < p_num; p++) {
		if (parray[p].status == PLAYER_EMPTY)
			continue;
		player_remove_observe(p, g);
	}

	player_remove_request(garray[g].white, garray[g].black, -1);
	player_remove_request(garray[g].black, garray[g].white, -1);

	player_save(garray[g].white);
	player_save(garray[g].black);

	return 0;
}

PUBLIC int
player_goto_board(int p, int board_num)
{
	int start, count = 0, on, g;

	if (board_num < 0 || board_num >= parray[p].simul_info.numBoards)
		return -1;

	if (parray[p].simul_info.boards[board_num] < 0)
		return -1;

	parray[p].simul_info.onBoard = board_num;
	parray[p].game = parray[p].simul_info.boards[board_num];
	parray[p].opponent = garray[parray[p].game].black;

	if (parray[p].simul_info.numBoards == 1)
		return 0;

	send_board_to(parray[p].game, p);

	start	= parray[p].game;
	on	= parray[p].simul_info.onBoard;

	do {
		if ((g = parray[p].simul_info.boards[on]) >= 0) {
			if (count == 0) {
				if (parray[garray[g].black].bell)
					pprintf(garray[g].black, "\007");

				pprintf(garray[g].black, "\n");
				pprintf_highlight(garray[g].black, "%s",
				    parray[p].name);
				pprintf_prompt(garray[g].black, " is at your "
				    "board!\n");
			} else if (count == 1) {
				if (parray[garray[g].black].bell)
					pprintf(garray[g].black, "\007");

				pprintf(garray[g].black, "\n");
				pprintf_highlight(garray[g].black, "%s",
				    parray[p].name);
				pprintf_prompt(garray[g].black, " will be at "
				    "your board NEXT!\n");
			} else {
				pprintf(garray[g].black, "\n");
				pprintf_highlight(garray[g].black, "%s",
				    parray[p].name);
				pprintf_prompt(garray[g].black, " is %d boards "
				    "away.\n", count);
			}

			count++;
		}

		if (++on >= parray[p].simul_info.numBoards)
			on = 0;
	} while (start != parray[p].simul_info.boards[on]);

	return 0;
}

PUBLIC int
player_goto_next_board(int p)
{
	int	g;
	int	on;
	int	start;

	on	= parray[p].simul_info.onBoard;
	start	= on;
	g	= -1;

	do {
		on++;

		if (on >= parray[p].simul_info.numBoards)
			on = 0;

		if ((g = parray[p].simul_info.boards[on]) >= 0)
			break;
	} while (start != on);

	if (g == -1) {
		pprintf(p, "\nMajor Problem! Can't find your next board.\n");
		return -1;
	}

	return player_goto_board(p, on);
}

PUBLIC int
player_goto_prev_board(int p)
{
	int	g;
	int	on;
	int	start;

	on	= parray[p].simul_info.onBoard;
	start	= on;
	g	= -1;

	do {
		--on;

		if (on < 0)
			on = (parray[p].simul_info.numBoards) - 1;

		if ((g = parray[p].simul_info.boards[on]) >= 0)
			break;
	} while (start != on);

	if (g == -1) {
		pprintf(p, "\nMajor Problem! Can't find your previous board."
		    "\n");
		return -1;
	}

	return player_goto_board(p, on);
}

PUBLIC int
player_goto_simulgame_bynum(int p, int num)
{
	int	g;
	int	on;
	int	start;

	on = parray[p].simul_info.onBoard;
	start = on;

	do {
		on++;

		if (on >= parray[p].simul_info.numBoards)
			on = 0;

		if ((g = parray[p].simul_info.boards[on]) == num)
			break;
	} while (start != on);

	if (g != num) {
		pprintf(p, "\nYou aren't playing that game!!\n");
		return -1;
	}

	return player_goto_board(p, on);
}

PUBLIC int
player_num_active_boards(int p)
{
	int count = 0;

	if (!parray[p].simul_info.numBoards)
		return 0;

	for (int i = 0; i < parray[p].simul_info.numBoards; i++) {
		if (parray[p].simul_info.boards[i] >= 0)
			count++;
	}

	return count;
}

PUBLIC int
player_num_results(int p, int result)
{
	int count = 0;

	if (!parray[p].simul_info.numBoards)
		return 0;

	for (int i = 0; i < parray[p].simul_info.numBoards; i++) {
		if (parray[p].simul_info.results[i] == result)
			count++;
	}

	return count;
}

PUBLIC int
player_simul_over(int p, int g, int result)
{
	char	tmp[1024];
	int	on, ong, p1, which = -1, won;

	for (won = 0; won < parray[p].simul_info.numBoards; won++) {
		if (parray[p].simul_info.boards[won] == g) {
			which = won;
			break;
		}
	}

	if (which == -1) {
		pprintf(p, "I can't find that game!\n");
		return -1;
	}

	pprintf(p, "\nBoard %d has completed.\n", (won + 1));

	on	= parray[p].simul_info.onBoard;
	ong	= parray[p].simul_info.boards[on];

	parray[p].simul_info.boards[won]	= -1;
	parray[p].simul_info.results[won]	= result;

	if (player_num_active_boards(p) == 0) {
		snprintf(tmp, sizeof tmp, "\n{Simul (%s vs. %d) is over.}\n"
		    "Results: %d Wins, %d Losses, %d Draws, %d Aborts\n",
		    parray[p].name,
		    parray[p].simul_info.numBoards,
		    player_num_results(p, RESULT_WIN),
		    player_num_results(p, RESULT_LOSS),
		    player_num_results(p, RESULT_DRAW),
		    player_num_results(p, RESULT_ABORT));

		for (p1 = 0; p1 < p_num; p1++) {
			if (parray[p].status != PLAYER_PROMPT)
				continue;
			if (!parray[p1].i_game && !player_is_observe(p1, g) &&
			    p1 != p)
				continue;
			pprintf_prompt(p1, "%s", tmp);
		}

		parray[p].simul_info.numBoards = 0;

		pprintf_prompt(p, "\nThat was the last board, thanks for "
		    "playing.\n");

		return 0;
	}

	if (ong == g) /* This game is over */
		player_goto_next_board(p);
	else
		player_goto_board(p, parray[p].simul_info.onBoard);

	pprintf_prompt(p, "\nThere are %d boards left.\n",
	    player_num_active_boards(p));
	return 0;
}

PRIVATE void
GetMsgFile(int p, char *fName, const size_t size, const char *func)
{
	int ret, too_long;

	ret = snprintf(fName, size, "%s/player_data/%c/%s.%s", stats_dir,
	    parray[p].login[0], parray[p].login, STATS_MESSAGES);
	too_long = (ret < 0 || (size_t)ret >= size);

	if (too_long) {
		fprintf(stderr, "FICS: %s: warning: snprintf truncated\n",
		    func);
	}
}

PUBLIC int
player_num_messages(int p)
{
	char fname[MAX_FILENAME_SIZE];

	if (!parray[p].registered)
		return 0;

	GetMsgFile(p, fname, sizeof fname, __func__);

	return lines_file(fname);
}

PUBLIC int
player_add_message(int top, int fromp, char *message)
{
	FILE	*fp;
	char	 fname[MAX_FILENAME_SIZE];
	char	 messbody[1024];
	char	 subj[256];
	time_t	 t = time(NULL);

	if (!parray[top].registered)
		return -1;
	if (!parray[fromp].registered)
		return -1;

	GetMsgFile(top, fname, sizeof fname, __func__);

	if (lines_file(fname) >= MAX_MESSAGES && parray[top].adminLevel == 0)
		return -1;

	if ((fp = fopen(fname, "a")) == NULL)
		return -1;
	fprintf(fp, "%s at %s: %s\n", parray[fromp].name, strltime(&t),
	    message);
	fclose(fp);

	pprintf(fromp, "\nThe following message was sent ");

	if (parray[top].i_mailmess) {
		snprintf(subj, sizeof subj, "FICS message from %s at FICS %s "
		    "(Do not reply by mail)", parray[fromp].name,
		    fics_hostname);
		snprintf(messbody, sizeof messbody, "%s at %s: %s\n",
		    parray[fromp].name, strltime(&t), message);
		mail_string_to_user(top, subj, messbody);
		pprintf(fromp, "(and emailed) ");
	}

	pprintf(fromp, "to %s:\n   %s\n", parray[top].name, message);
	return 0;
}

PUBLIC void
SaveTextListEntry(textlist **Entry, char *string, int n)
{
	*Entry = rmalloc(sizeof(textlist));

	(*Entry)->text	= xstrdup(string);
	(*Entry)->index	= n;
	(*Entry)->next	= NULL;
}

PUBLIC textlist *
ClearTextListEntry(textlist *entry)
{
	textlist *ret = entry->next;

	strfree(entry->text);
	rfree(entry);
	return ret;
}

PUBLIC void
ClearTextList(textlist *head)
{
	for (textlist *cur = head; cur != NULL; cur = ClearTextListEntry(cur)) {
		/* null */;
	}
}

/*
 * if which = 0 load all messages;
 * if it's (p1 + 1) load messages only from p1;
 * if it's -(p1 + 1) load all messages EXCEPT those from p1.
 */
PRIVATE int
SaveThisMsg(int which, char *line)
{
	char	Sender[MAX_LOGIN_NAME] = { '\0' };
	int	p1;

	_Static_assert(19 < ARRAY_SIZE(Sender), "Array too small");

	if (which == 0)
		return 1;

	if (sscanf(line, "%19s", Sender) != 1) {
		warnx("%s: failed to read sender", __func__);
		return 0;
	}

	if (which < 0) {
		p1 = (-which) - 1;
		return strcmp(Sender, parray[p1].name);
	}

	p1 = (which - 1);
	return !strcmp(Sender, parray[p1].name);
}

/*
 * which = 0 to load all messages;
 * it's (p1 + 1) to load messages only from p1;
 * and it's -(p1 + 1) to load all messages EXCEPT those from p1.
 */
PRIVATE int
LoadMsgs(int p, int which, textlist **Head)
{
	FILE		*fp;
	char		 fName[MAX_FILENAME_SIZE];
	char		 line[MAX_LINE_SIZE];
	int		 n = 0, nSave = 0;
	textlist**	 Cur = Head;

	*Head = NULL;
	GetMsgFile(p, fName, sizeof fName, __func__);

	if ((fp = fopen(fName, "r")) == NULL)
		return -1;

	while (fgets(line, sizeof line, fp) != NULL) {
		if (SaveThisMsg(which, line)) {
			SaveTextListEntry(Cur, line, ++n);
			Cur = &(*Cur)->next;
			nSave++;
		} else
			n++;
	}

	fclose(fp);
	return nSave;
}

/*
 * start > 0 and end > start (or end = 0) to save messages in range;
 * start < 0 and end < start (or end = 0) to clear messages in range;
 * if end = 0, range goes to end of file (not tested yet).
 */
PRIVATE int
LoadMsgRange(int p, int start, int end, textlist **Head)
{
	FILE		*fp;
	char		 fName[MAX_FILENAME_SIZE];
	char		 line[MAX_LINE_SIZE];
	int		 n = 1, nSave = 0, nKill = 0;
	textlist**	 Cur = Head;

	*Head = NULL;
	GetMsgFile(p, fName, sizeof fName, __func__);

	if ((fp = fopen(fName, "r")) == NULL) {
		pprintf(p, "You have no messages.\n");
		return -1;
	}

	for (n = 1; n <= end || end <= 0; n++) {
		if (fgets(line, sizeof line, fp) == NULL)
			break;
		if ((start < 0 && (n < -start || n > -end)) ||
		    (start >= 0 && n >= start)) {
			SaveTextListEntry(Cur, line, n);
			Cur = &(*Cur)->next;
			nSave++;
		} else
			nKill++;
	}

	fclose(fp);

	if (start < 0) {
		if (n <= -start)
			pprintf(p, "You do not have a message %d.\n", -start);
		return nKill;
	} else {
		if (n <= start)
			pprintf(p, "You do not have a message %d.\n", start);
		return nSave;
	}
}

PRIVATE int
WriteMsgFile(int p, textlist *Head)
{
	FILE		*fp;
	char		 fName[MAX_FILENAME_SIZE];
	textlist	*Cur;

	GetMsgFile(p, fName, sizeof fName, __func__);

	if ((fp = fopen(fName, "w")) == NULL)
		return 0;
	for (Cur = Head; Cur != NULL; Cur = Cur->next)
		fprintf(fp, "%s", Cur->text);
	fclose(fp);
	return 1;
}

PUBLIC int
ClearMsgsBySender(int p, param_list param)
{
	int		 nFound;
	int		 p1, connected;
	textlist	*Head;

	if (!FindPlayer(p, param[0].val.word, &p1, &connected))
		return -1;

	nFound = LoadMsgs(p, -(p1 + 1), &Head);

	if (nFound < 0) {
		pprintf(p, "You have no messages.\n");
	} else if (nFound == 0) {
		pprintf(p, "You have no messages from %s.\n", parray[p1].name);
	} else {
		if (WriteMsgFile(p, Head)) {
			pprintf(p, "Messages from %s cleared.\n",
			    parray[p1].name);
		} else {
			pprintf(p, "Problem writing message file; "
			    "please contact an admin.\n");
			fprintf(stderr, "Problem writing message file for "
			    "%s.\n", parray[p].name);
		}

		ClearTextList(Head);
	}

	if (!connected)
		player_remove(p1);
	return nFound;
}

PRIVATE void
ShowTextList(int p, textlist *Head, int ShowIndex)
{
	textlist *CurMsg;

	if (ShowIndex) {
		for (CurMsg = Head; CurMsg != NULL; CurMsg = CurMsg->next)
			pprintf(p, "%2d. %s", CurMsg->index, CurMsg->text);
	} else {
		for (CurMsg = Head; CurMsg != NULL; CurMsg = CurMsg->next)
			pprintf(p, "%s", CurMsg->text);
	}
}

PUBLIC int
player_show_messages(int p)
{
	int		 n;
	textlist	*Head;

	n = LoadMsgs(p, 0, &Head);

	if (n <= 0) {
		pprintf(p, "You have no messages.\n");
		return -1;
	} else {
		pprintf(p, "Messages:\n");
		ShowTextList(p, Head, 1);
		ClearTextList(Head);
		return 0;
	}
}

PUBLIC int
ShowMsgsBySender(int p, param_list param)
{
	int		 nFrom, nTo;
	int		 p1, connected;
	textlist	*Head;

	if (!FindPlayer(p, param[0].val.word, &p1, &connected))
		return -1;

	if (!parray[p1].registered) {
		pprintf(p, "Player \"%s\" is unregistered and cannot send or "
		    "receive messages.\n", parray[p1].name);
		return -1; /* no need to disconnect */
	}

	nFrom = nTo = -1;

	if (p != p1) {
		if ((nTo = LoadMsgs(p1, p + 1, &Head)) <= 0) {
			pprintf(p, "%s has no messages from you.\n",
			    parray[p1].name);
		} else {
			pprintf(p, "Messages to %s:\n", parray[p1].name);
			ShowTextList(p, Head, 0);
			ClearTextList(Head);
		}
	}

	if ((nFrom = LoadMsgs(p, p1 + 1, &Head)) <= 0) {
		pprintf(p, "\nYou have no messages from %s.\n",
		    parray[p1].name);
	} else {
		pprintf(p, "Messages from %s:\n", parray[p1].name);
		ShowTextList(p, Head, 1);
		ClearTextList(Head);
	}

	if (!connected)
		player_remove(p1);
	return (nFrom > 0 || nTo > 0);
}

PUBLIC int
ShowMsgRange(int p, int start, int end)
{
	int		 n;
	textlist	*Head;

	if ((n = LoadMsgRange(p, start, end, &Head)) > 0) {
		ShowTextList(p, Head, 1);
		ClearTextList(Head);
	}

	return n;
}

PUBLIC int
ClrMsgRange(int p, int start, int end)
{
	int		 n;
	textlist	*Head;

	n = LoadMsgRange(p, -start, -end, &Head);

	if (n > 0) {
		if (WriteMsgFile(p, Head))
			pprintf(p, "Message %d cleared.\n", start);
	}

	if (n >= 0)
		ClearTextList(Head);
	return n;
}

PUBLIC int
player_clear_messages(int p)
{
	char fname[MAX_FILENAME_SIZE];

	if (!parray[p].registered)
		return -1;
	GetMsgFile(p, fname, sizeof fname, __func__);
	unlink(fname);
	return 0;
}

/*
 * Find player matching the given string.
 *
 * First looks for exact match with a logged in player, then an exact
 * match with a registered player, then a partial unique match with a
 * logged in player, then a partial match with a registered player.
 *
 * Returns player number if the player is connected. Negative (player
 * number) if the player had to be connected and 0 if no player was
 * found.
 */
PUBLIC int
player_search(int p, char *name)
{
	char	*buffer[1000];
	char	 pdir[MAX_FILENAME_SIZE];
	int	 p1, count;

	// Exact match with connected player?
	if ((p1 = player_find_bylogin(name)) >= 0)
		return (p1 + 1);

	// Exact match with registered player?
	snprintf(pdir, sizeof pdir, "%s/%c", player_dir, name[0]);
	count = search_directory(pdir, name, buffer, ARRAY_SIZE(buffer));

	if (count > 0 && !strcmp(name, *buffer))
		goto ReadPlayerFromFile;	// Found an unconnected
						// registered player

	// Partial match with connected player?
	if ((p1 = player_find_part_login(name)) >= 0) {
		return (p1 + 1);
	} else if (p1 == -2) {
		// Ambiguous. Matches too many connected players.
		pprintf(p, "Ambiguous name '%s'; matches more than one player."
		    "\n", name);
		return 0;
	}

	// Partial match with registered player?
	if (count < 1) {
		pprintf(p, "There is no player matching that name.\n");
		return 0;
	} else if (count > 1) {
		pprintf(p, "-- Matches: %d names --", count);
		display_directory(p, buffer, count);
		return 0;
	}

  ReadPlayerFromFile:

	p1 = player_new();

	if (player_read(p1, *buffer)) {
		player_remove(p1);
		pprintf(p, "ERROR: a player named %s was expected but not "
		    "found!\n", *buffer);
		pprintf(p, "Please tell an admin about this incident. "
		    "Thank you.\n");
		return 0;
	}

	return (-p1) - 1;	// Negative to indicate player was not connected
}

PUBLIC int
player_kill(char *name)
{
	char	fname[MAX_FILENAME_SIZE];
	char	fname2[MAX_FILENAME_SIZE];

	snprintf(fname, sizeof fname, "%s/%c/%s", player_dir, name[0],
	    name);
	snprintf(fname2, sizeof fname2, "%s/%c/.rem.%s", player_dir, name[0],
	    name);
	rename(fname, fname2);

	RemHist(name);

	snprintf(fname, sizeof fname, "%s/player_data/%c/%s.games",
	    stats_dir, name[0], name);
	snprintf(fname2, sizeof fname2, "%s/player_data/%c/.rem.%s.games",
	    stats_dir, name[0], name);
	rename(fname, fname2);

	snprintf(fname, sizeof fname, "%s/player_data/%c/%s.comments",
	    stats_dir, name[0], name);
	snprintf(fname2, sizeof fname2, "%s/player_data/%c/.rem.%s.comments",
	    stats_dir, name[0], name);
	rename(fname, fname2);

	snprintf(fname, sizeof fname, "%s/player_data/%c/%s.logons",
	    stats_dir, name[0], name);
	snprintf(fname2, sizeof fname2, "%s/player_data/%c/.rem.%s.logons",
	    stats_dir, name[0], name);
	rename(fname, fname2);

	snprintf(fname, sizeof fname, "%s/player_data/%c/%s.messages",
	    stats_dir, name[0], name);
	snprintf(fname2, sizeof fname2, "%s/player_data/%c/.rem.%s.messages",
	    stats_dir, name[0], name);
	rename(fname, fname2);

	return 0;
}

PUBLIC int
player_rename(char *name, char *newname)
{
	char	fname[MAX_FILENAME_SIZE];
	char	fname2[MAX_FILENAME_SIZE];

	snprintf(fname, sizeof fname, "%s/%c/%s", player_dir, name[0],
	    name);
	snprintf(fname2, sizeof fname2, "%s/%c/%s", player_dir, newname[0],
	    newname);
	rename(fname, fname2);

	snprintf(fname, sizeof fname, "%s/player_data/%c/%s.games",
	    stats_dir, name[0], name);
	snprintf(fname2, sizeof fname2, "%s/player_data/%c/%s.games",
	    stats_dir, newname[0], newname);
	rename(fname, fname2);

	snprintf(fname, sizeof fname, "%s/player_data/%c/%s.comments",
	    stats_dir, name[0], name);
	snprintf(fname2, sizeof fname2, "%s/player_data/%c/%s.comments",
	    stats_dir, newname[0], newname);
	rename(fname, fname2);

	snprintf(fname, sizeof fname, "%s/player_data/%c/%s.logons",
	    stats_dir, name[0], name);
	snprintf(fname2, sizeof fname2, "%s/player_data/%c/%s.logons",
	    stats_dir, newname[0], newname);
	rename(fname, fname2);

	snprintf(fname, sizeof fname, "%s/player_data/%c/%s.messages",
	    stats_dir, name[0], name);
	snprintf(fname2, sizeof fname2, "%s/player_data/%c/%s.messages",
	    stats_dir, newname[0], newname);
	rename(fname, fname2);

	return 0;
}

PUBLIC int
player_raise(char *name)
{
	char	fname[MAX_FILENAME_SIZE];
	char	fname2[MAX_FILENAME_SIZE];

	snprintf(fname, sizeof fname, "%s/%c/%s", player_dir,
	    name[0], name);
	snprintf(fname2, sizeof fname2, "%s/%c/.rem.%s", player_dir,
	    name[0], name);
	rename(fname2, fname);

	snprintf(fname, sizeof fname, "%s/player_data/%c/%s.games",
	    stats_dir, name[0], name);
	snprintf(fname2, sizeof fname2, "%s/player_data/%c/.rem.%s.games",
	    stats_dir, name[0], name);
	rename(fname2, fname);

	snprintf(fname, sizeof fname, "%s/player_data/%c/%s.comments",
	    stats_dir, name[0], name);
	snprintf(fname2, sizeof fname2, "%s/player_data/%c/.rem.%s.comments",
	    stats_dir, name[0], name);
	rename(fname2, fname);

	snprintf(fname, sizeof fname, "%s/player_data/%c/%s.logons",
	    stats_dir, name[0], name);
	snprintf(fname2, sizeof fname2, "%s/player_data/%c/.rem.%s.logons",
	    stats_dir, name[0], name);
	rename(fname2, fname);

	snprintf(fname, sizeof fname, "%s/player_data/%c/%s.messages",
	    stats_dir, name[0], name);
	snprintf(fname2, sizeof fname2, "%s/player_data/%c/.rem.%s.messages",
	    stats_dir, name[0], name);
	rename(fname2, fname);

	return 0;
}

PUBLIC int
player_reincarn(char *name, char *newname)
{
	char	fname[MAX_FILENAME_SIZE];
	char	fname2[MAX_FILENAME_SIZE];

	snprintf(fname, sizeof fname, "%s/%c/%s", player_dir,
	    newname[0], newname);
	snprintf(fname2, sizeof fname2, "%s/%c/.rem.%s", player_dir,
	    name[0], name);
	rename(fname2, fname);

	snprintf(fname, sizeof fname, "%s/player_data/%c/%s.games",
	    stats_dir, newname[0], newname);
	snprintf(fname2, sizeof fname2, "%s/player_data/%c/.rem.%s.games",
	    stats_dir, name[0], name);
	rename(fname2, fname);

	snprintf(fname, sizeof fname, "%s/player_data/%c/%s.comments",
	    stats_dir, newname[0], newname);
	snprintf(fname2, sizeof fname2, "%s/player_data/%c/.rem.%s.comments",
	    stats_dir, name[0], name);
	rename(fname2, fname);

	snprintf(fname, sizeof fname, "%s/player_data/%c/%s.logons",
	    stats_dir, newname[0], newname);
	snprintf(fname2, sizeof fname2, "%s/player_data/%c/.rem.%s.logons",
	    stats_dir, name[0], name);
	rename(fname2, fname);

	snprintf(fname, sizeof fname, "%s/player_data/%c/%s.messages",
	    stats_dir, newname[0], newname);
	snprintf(fname2, sizeof fname2, "%s/player_data/%c/.rem.%s.messages",
	    stats_dir, name[0], name);
	rename(fname2, fname);

	return 0;
}

PUBLIC int
player_num_comments(int p)
{
	char fname[MAX_FILENAME_SIZE];

	if (!parray[p].registered)
		return 0;
	snprintf(fname, sizeof fname, "%s/player_data/%c/%s.%s", stats_dir,
	    parray[p].login[0], parray[p].login, "comments");
	return lines_file(fname);
}

PUBLIC int
player_add_comment(int p_by, int p_to, char *comment)
{
	FILE	*fp;
	char	 fname[MAX_FILENAME_SIZE];
	time_t	 t = time(NULL);

	if (!parray[p_to].registered)
		return -1;

	snprintf(fname, sizeof fname, "%s/player_data/%c/%s.%s", stats_dir,
	    parray[p_to].login[0], parray[p_to].login, "comments");

	if ((fp = fopen(fname, "a")) == NULL)
		return -1;

	fprintf(fp, "%s at %s: %s\n", parray[p_by].name, strltime(&t), comment);
	fclose(fp);
	parray[p_to].num_comments = player_num_comments(p_to);
	return 0;
}

PUBLIC int
player_show_comments(int p, int p1)
{
	char fname[MAX_FILENAME_SIZE];

	snprintf(fname, sizeof fname, "%s/player_data/%c/%s.%s", stats_dir,
	    parray[p1].login[0], parray[p1].login, "comments");
	psend_file(p, NULL, fname);
	return 0;
}

/*
 * Returns 1 if player is head admin and 0 otherwise.
 */
PUBLIC int
player_ishead(int p)
{
	return (!strcasecmp(parray[p].name, hadmin_handle));
}