/* comproc.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
   foxbat			95/03/11	Added filters in cmatch
   Markus Uhlin			23/12/09	Fixed implicit ints
   Markus Uhlin			23/12/17	Fixed compiler warnings
   Markus Uhlin			23/12/17	Usage of 'time_t'
   Markus Uhlin			23/12/17	Reformatted com_stats()
   Markus Uhlin			23/12/19	Usage of 'time_t'
   Markus Uhlin			23/12/23	Fixed crypt()
   Markus Uhlin			24/01/03	Added usage of ARRAY_SIZE() and
						reformatted functions.
   Markus Uhlin			24/01/06	Fixed potentially insecure
						format strings.
   Markus Uhlin			24/03/23	Size-bounded string handling
						plus truncation checks.
*/

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

#include <sys/resource.h>

#include <err.h>

#include "board.h"
#include "command.h"
#include "comproc.h"
#include "config.h"
#include "eco.h"
#include "fics_getsalt.h"
#include "ficsmain.h"
#include "formula.h"
#include "gamedb.h"
#include "gameproc.h"
#include "lists.h"
#include "multicol.h"
#include "network.h"
#include "obsproc.h"
#include "playerdb.h"
#include "ratings.h"
#include "rmalloc.h"
#include "talkproc.h"
#include "utils.h"
#include "variable.h"

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

#define WHO_OPEN         0x01
#define WHO_CLOSED       0x02
#define WHO_RATED        0x04
#define WHO_UNRATED      0x08
#define WHO_FREE         0x10
#define WHO_PLAYING      0x20
#define WHO_REGISTERED   0x40
#define WHO_UNREGISTERED 0x80
#define WHO_BUGTEAM      0x100

#define WHO_ALL 0xff

typedef int ((*who_cmp_t)(const void *, const void *));

PUBLIC const int	none = 0;
PUBLIC const int	blitz_rat = 1;
PUBLIC const int	std_rat = 2;
PUBLIC const int	wild_rat = 3;
PUBLIC const int	light_rat = 4;

PUBLIC int num_news = -1;

PRIVATE char *inout_string[] = { "login", "logout" };

PUBLIC int
com_rating_recalc(int p, param_list param)
{
	(void) p;	// maybe not referenced
	(void) param;	// not referenced

	ASSERT(parray[p].adminLevel >= ADMIN_ADMIN);
	rating_recalc();
	return COM_OK;
}

PUBLIC int
com_more(int p, param_list param)
{
	// not referenced
	(void) param;

	pmore_file(p);
	return COM_OK;
}

PUBLIC void
rscan_news2(FILE *fp, int p, int num)
{
	char		*junkp;
	char		 count[10] = { '\0' };
	char		 junk[MAX_LINE_SIZE] = { '\0' };
	long int	 lval;
	time_t		 crtime;

	if (num == 0)
		return;

	if (fgets(junk, sizeof junk, fp) == NULL || feof(fp) ||
	    sscanf(junk, "%ld %9s", &lval, count) != 2)
		return;

	rscan_news2(fp, p, num - 1);

	junkp = junk;
	junkp = nextword(junkp);
	junkp = nextword(junkp);

	crtime = lval;
	pprintf(p, "%3s (%s) %s", count, fix_time(strltime(&crtime)), junkp);
}

PUBLIC int
com_news(int p, param_list param)
{
	FILE		*fp;
	char		*junkp;
	char		 count[10] = { '\0' };
	char		 filename[MAX_FILENAME_SIZE] = { '\0' };
	char		 junk[MAX_LINE_SIZE] = { '\0' };
	int		 found = 0;
	long int	 lval;
	time_t		 crtime;

	snprintf(filename, sizeof filename, "%s/newnews.index", news_dir);

	if ((fp = fopen(filename, "r")) == NULL) {
		fprintf(stderr, "Can\'t find news index.\n");
		return COM_OK;
	}

	if (param[0].type == 0) {
		/*
		 * No params - then just display index of last ten
		 * news items.
		 */

		pprintf(p,"Index of recent news items:\n");
		fgets(junk, MAX_LINE_SIZE, fp);
		sscanf(junk, "%ld %s", &lval, count);
		rscan_news2(fp, p, 9);

		junkp = junk;
		junkp = nextword(junkp);
		junkp = nextword(junkp);

		crtime = lval;
		pprintf(p, "%3s (%s) %s", count, fix_time(strltime(&crtime)),
		    junkp);
		fclose(fp);
	} else if (param[0].type == TYPE_WORD &&
	    !strcmp(param[0].val.word, "all")) {
		/*
		 * Param all - displays index all news items.
		 */

		pprintf(p, "Index of all news items:\n");
		fgets(junk, MAX_LINE_SIZE, fp);
		sscanf(junk, "%ld %s", &lval, count);
		rscan_news(fp, p, 0);

		junkp = junk;
		junkp = nextword(junkp);
		junkp = nextword(junkp);

		crtime = lval;
		pprintf(p, "%3s (%s) %s", count, fix_time(strltime(&crtime)),
		    junkp);
		fclose(fp);
	} else {
		/*
		 * Check if the specific news file exist in index.
		 */

		while (!feof(fp) && !found) {
			junkp = junk;

			if (fgets(junk, sizeof junk, fp) == NULL || feof(fp))
				break;

			sscanf(junkp, "%ld %s", &lval, count);
			crtime = lval;

			if (!strcmp(count, param[0].val.word)) {
				found = 1;

				junkp = nextword(junkp);
				junkp = nextword(junkp);

				pprintf(p, "NEWS %3s (%s) %s\n", count,
				    fix_time(strltime(&crtime)), junkp);
			}
		}

		fclose(fp);

		if (!found) {
			pprintf(p, "Bad index number!\n");
			return COM_OK;
		}

		/*
		 * File exists - show it
		 */

		snprintf(filename, sizeof filename, "%s/news.%s", news_dir,
		    param[0].val.word);

		if ((fp = fopen(filename, "r")) == NULL) {
			pprintf(p, "No more info.\n");
			return COM_OK;
		}

		fclose(fp);
		snprintf(filename, sizeof filename, "news.%s",
		    param[0].val.word);

		if (psend_file(p, news_dir, filename) < 0) {
			pprintf(p, "Internal error - couldn't send news file!"
			    "\n");
		}
	}

	return COM_OK;
}

PUBLIC int
com_quit(int p, param_list param)
{
	// not referenced
	(void) param;

	if (parray[p].game >= 0 && garray[parray[p].game].status ==
	    GAME_EXAMINE)
		pcommand(p, "unexamine");
	if (parray[p].game >= 0) {
		pprintf(p, "You can't quit while you are playing a game.\n"
		    "Type 'resign' to resign the game, or you can request an "
		    "abort with 'abort'.\n");
		return COM_OK;
	}

	psend_logoutfile(p, mess_dir, MESS_LOGOUT);
	return COM_LOGOUT;
}

PUBLIC int
com_set(int p, param_list param)
{
	char	*val;
	int	 result;
	int	 which;

	if (param[1].type == TYPE_NULL)
		val = NULL;
	else
		val = param[1].val.string;

	result = var_set(p, param[0].val.word, val, &which);

	switch (result) {
	case VAR_OK:
		break;
	case VAR_BADVAL:
		pprintf(p, "Bad value given for variable %s.\n",
		    param[0].val.word);
		break;
	case VAR_NOSUCH:
		pprintf(p, "No such variable name %s.\n", param[0].val.word);
		break;
	case VAR_AMBIGUOUS:
		pprintf(p, "Ambiguous variable name %s.\n", param[0].val.word);
		break;
	}

	return COM_OK;
}

PUBLIC int
FindPlayer(int p, char *name, int *p1, int *connected)
{
	*p1 = player_search(p, name);

	if (*p1 == 0)
		return 0;
	if (*p1 < 0) {	// The player had to be connected and will be removed
			// later.
		*connected = 0;
		*p1 = (-*p1) - 1;
	} else {
		*connected = 1;
		*p1 = *p1 - 1;
	}

	return 1;
}

PRIVATE void
com_stats_andify(int *numbers, int howmany, char *dest, size_t dsize)
{
	char tmp[10] = { '\0' };

	*dest = '\0';

	while (howmany--) {
		snprintf(tmp, sizeof tmp, "%d", numbers[howmany]);
		strlcat(dest, tmp, dsize);

		if (howmany > 1)
			strlcpy(tmp, ", ", sizeof tmp);
		else if (howmany == 1)
			strlcpy(tmp, " and ", sizeof tmp);
		else
			strlcpy(tmp, ".\n", sizeof tmp);

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

PRIVATE void
com_stats_rating(char *hdr, statistics *stats, char *dest, const size_t dsize)
{
	char tmp[100] = { '\0' };

	snprintf(dest, dsize, "%-10s%4s    %5.1f   %4d   %4d   %4d   %4d", hdr,
	    ratstr(stats->rating),
	    stats->sterr,
	    stats->win,
	    stats->los,
	    stats->dra,
	    stats->num);

	if (stats->whenbest) {
		snprintf(tmp, sizeof tmp, "   %d", stats->best);
		strlcat(dest, tmp, dsize);
		strftime(tmp, sizeof tmp, " (%d-%b-%y)",
		    localtime((time_t *) &stats->whenbest));
		strlcat(dest, tmp, dsize);
	}

	if (strlcat(dest, "\n", dsize) >= dsize) {
		(void) fprintf(stderr, "FICS: %s (line %d): warning: strlcat() "
		    "truncated\n", __func__, __LINE__);
	}
}

PUBLIC int
com_stats(int p, param_list param)
{
#define NUMBERS_SIZE \
	(MAX_OBSERVE > MAX_SIMUL ? MAX_OBSERVE : MAX_SIMUL)
	char		 line[255] = { '\0' };
	char		 tmp[255] = { '\0' };
	int		 g, i;
	int		 numbers[NUMBERS_SIZE];
	int		 onTime;
	int		 p1, connected;
	time_t		 t;

	if (param[0].type == TYPE_WORD) {
		if (!FindPlayer(p, param[0].val.word, &p1, &connected))
			return COM_OK;
	} else {
		p1 = p;
		connected = 1;
	}

	snprintf(line, sizeof line, "\nStatistics for %-11s ", parray[p1].name);

	if (connected && parray[p1].status == PLAYER_PROMPT) {
		snprintf(tmp, sizeof tmp, "On for: %s",
		    hms_desc(player_ontime(p1)));
		strlcat(line, tmp, sizeof line);
		snprintf(tmp, sizeof tmp, "   Idle: %s\n",
		    hms_desc(player_idle(p1)));
	} else {
		if ((t = player_lastdisconnect(p1))) {
			snprintf(tmp, sizeof tmp, "(Last disconnected %s):\n",
			    strltime(&t));
		} else
			strlcpy(tmp, "(Never connected.)\n", sizeof tmp);
	}

	strlcat(line, tmp, sizeof line);
	pprintf(p, "%s", line);

	if (parray[p1].simul_info.numBoards) {
		t = 0;
		i = 0;

		while (i < parray[p1].simul_info.numBoards) {
			if ((numbers[t] = parray[p1].simul_info.boards[i] + 1) != 0)
				t++;
			i++;
		}
		pprintf(p, "%s is giving a simul: game%s ", parray[p1].name,
		    (t > 1 ? "s" : ""));
		com_stats_andify(numbers, t, tmp, sizeof tmp);
		pprintf(p, "%s", tmp);
	} else if (parray[p1].game >= 0) {
		g = parray[p1].game;

		if (garray[g].status == GAME_EXAMINE) {
			pprintf(p, "(Examining game %d: %s vs. %s)\n", (g + 1),
			    garray[g].white_name,
			    garray[g].black_name);
		} else {
			pprintf(p, "(playing game %d: %s vs. %s)\n", (g + 1),
			    parray[garray[g].white].name,
			    parray[garray[g].black].name);

			if (garray[g].link >= 0) {
				pprintf(p, "(partner is playing game %d: "
				    "%s vs. %s)\n",
				    (garray[g].link + 1),
				    parray[garray[garray[g].link].white].name,
				    parray[garray[garray[g].link].black].name);
			}
		}
	}

	if (parray[p1].num_observe) {
		t = 0;
		i = 0;

		while (i < parray[p1].num_observe) {
			if ((g = parray[p1].observe_list[i]) != -1 &&
			    (parray[p].adminLevel >= ADMIN_ADMIN ||
			    garray[g].private == 0))
				numbers[t++] = (g + 1);
			i++;
		}

		if (t) {
			pprintf(p, "%s is observing game%s ", parray[p1].name,
			    (t > 1 ? "s" : ""));
			com_stats_andify(numbers, t, tmp, sizeof tmp);
			pprintf(p, "%s", tmp);
		}
	}

	if (parray[p1].busy[0])
		pprintf(p, "(%s %s)\n", parray[p1].name, parray[p1].busy);

	if (!parray[p1].registered) {
		pprintf(p, "%s is NOT a registered player.\n\n",
		    parray[p1].name);
	} else {
		pprintf(p, "\n         rating     RD     win   loss   draw  "
		    "total   best\n");

		com_stats_rating("Blitz", &parray[p1].b_stats, tmp, sizeof tmp);
		pprintf(p, "%s", tmp);
		com_stats_rating("Standard", &parray[p1].s_stats, tmp,
		    sizeof tmp);
		pprintf(p, "%s", tmp);
		com_stats_rating("Lightning", &parray[p1].l_stats, tmp,
		    sizeof tmp);
		pprintf(p, "%s", tmp);
		com_stats_rating("Wild", &parray[p1].w_stats, tmp, sizeof tmp);
		pprintf(p, "%s", tmp);
	}

	pprintf(p, "\n");

	if (parray[p1].adminLevel > 0) {
		pprintf(p, "Admin Level: ");

		switch (parray[p1].adminLevel) {
		case 5:
			pprintf(p, "Authorized Helper Person\n");
			break;
		case 10:
			pprintf(p, "Administrator\n");
			break;
		case 15:
			pprintf(p, "Help File Librarian/Administrator\n");
			break;
		case 20:
			pprintf(p, "Master Administrator\n");
			break;
		case 50:
			pprintf(p, "Master Help File Librarian/Administrator"
			    "\n");
			break;
		case 60:
			pprintf(p, "Assistant Super User\n");
			break;
		case 100:
			pprintf(p, "Super User\n");
			break;
		default:
			pprintf(p, "%d\n", parray[p1].adminLevel);
			break;
		}
	}

	// Full Name
	if (parray[p].adminLevel > 0) {
		pprintf(p, "Full Name  : %s\n", (parray[p1].fullName ?
		    parray[p1].fullName : "(none)"));
	}

	// Address
	if ((p1 == p && parray[p1].registered) || parray[p].adminLevel > 0) {
		pprintf(p, "Address    : %s\n", (parray[p1].emailAddress ?
		    parray[p1].emailAddress : "(none)"));
	}

	// Host
	if (parray[p].adminLevel > 0) {
		pprintf(p, "Host       : %s\n", dotQuad(connected ?
		    parray[p1].thisHost : parray[p1].lastHost));
	}

	// Comments
	if (parray[p].adminLevel > 0 && parray[p1].registered) {
		if (parray[p1].num_comments) {
			pprintf(p, "Comments   : %d\n",
			    parray[p1].num_comments);
		}
	}

	if (connected &&
	    parray[p1].registered &&
	    (p == p1 || parray[p].adminLevel > 0)) {
		char *timeToStr = ctime((time_t *) &parray[p1].timeOfReg);

		timeToStr[strlen(timeToStr) - 1] = '\0';
		pprintf(p, "\n");

		onTime = ((time(NULL) - parray[p1].logon_time) +
		    parray[p1].totalTime);

		pprintf(p, "Total time on-line: %s\n", hms_desc(onTime) );
		pprintf(p, "%% of life on-line:  %3.1f  (since %s)\n", // XXX
		    (double) ((onTime * 100) / (double) (time(NULL) -
		    parray[p1].timeOfReg)),
		    timeToStr);
	}

#ifdef TIMESEAL
	if (connected) {
		pprintf(p, "\nTimeseal   : %s\n",
		    (con[parray[p1].socket].timeseal ? "On" : "Off"));
	}
	if (parray[p].adminLevel > 0 && connected) {
		if (findConnection(parray[p1].socket) &&
		    con[parray[p1].socket].timeseal) {
			pprintf(p, "Unix acc   : %s\nSystem/OS  : %s\n",
			    con[parray[p1].socket].user,
			    con[parray[p1].socket].sys);
		}
	}
#endif

	if (parray[p1].num_plan) {
		pprintf(p, "\n");

		for (i = 0; i < parray[p1].num_plan; i++) {
			pprintf(p, "%2d: %s\n", (i + 1),
			    (parray[p1].planLines[i] != NULL
			    ? parray[p1].planLines[i]
			    : ""));
		}
	}

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

PUBLIC int
com_password(int p, param_list param)
{
	char	*oldpassword = param[0].val.word;
	char	*newpassword = param[1].val.word;
	char	 salt[FICS_SALT_SIZE];

	if (!parray[p].registered) {
		pprintf(p, "Setting a password is only for registered players."
		    "\n");
		return COM_OK;
	}

	if (parray[p].passwd) {
		strncpy(salt, &(parray[p].passwd[0]), sizeof salt - 1);
		salt[sizeof salt - 1] = '\0';

		if (strcmp(crypt(oldpassword, salt), parray[p].passwd)) {
			pprintf(p, "Incorrect password, password not changed!"
			    "\n");
			return COM_OK;
		}

		rfree(parray[p].passwd);
		parray[p].passwd = NULL;
	}

	strlcpy(salt, fics_getsalt(), sizeof salt);
	parray[p].passwd = xstrdup(crypt(newpassword, salt));

	pprintf(p, "Password changed to \"%s\".\n", newpassword);
	return COM_OK;
}

PUBLIC int
com_uptime(int p, param_list param)
{
	int days, hours, mins, secs;
	struct rusage ru;
	unsigned long int uptime = (time(NULL) - startuptime);

	(void) param; // XXX: unused

	days	= (uptime / (60*60*24));
	hours	= ((uptime % (60*60*24)) / (60*60));
	mins	= (((uptime % (60*60*24)) % (60*60)) / 60);
	secs	= (((uptime % (60*60*24)) % (60*60)) % 60);

	pprintf(p, "Server location: %s   Server version : %s\n", fics_hostname,
	    VERS_NUM);
	pprintf(p, "The server has been up since %s.\n",
	    strltime(&startuptime));

	if (days == 0 && hours == 0 && mins == 0) {
		pprintf(p, "(Up for %d second%s)\n",
		    secs, (secs == 1 ? "" : "s"));
	} else if (days == 0 && hours == 0) {
		pprintf(p, "(Up for %d minute%s and %d second%s)\n",
		    mins, (mins == 1 ? "" : "s"),
		    secs, (secs == 1 ? "" : "s"));
	} else if (days == 0) {
		pprintf(p, "(Up for %d hour%s, %d minute%s and %d second%s)\n",
		    hours, (hours == 1 ? "" : "s"),
		    mins, (mins == 1 ? "" : "s"),
		    secs, (secs == 1 ? "" : "s"));
	} else {
		pprintf(p, "(Up for %d day%s, %d hour%s, %d minute%s and %d "
		    "second%s)\n",
		    days, (days == 1 ? "" : "s"),
		    hours, (hours == 1 ? "" : "s"),
		    mins, (mins == 1 ? "" : "s"),
		    secs, (secs == 1 ? "" : "s"));
	}

	pprintf(p, "\nAllocs: %u  Frees: %u  Allocs In Use: %u\n",
	    malloc_count, free_count, (malloc_count - free_count));
	if (parray[p].adminLevel >= ADMIN_ADMIN) {
		pprintf(p, "\nplayer size:%zu, game size:%zu, con size:%d, "
		    "g_num:%d\n", sizeof(player), sizeof(game), net_consize(),
		    g_num);

		getrusage(RUSAGE_SELF, &ru);

		pprintf(p, "pagesize = %d, maxrss = %ld, total = %ld\n",
		    getpagesize(),
		    ru.ru_maxrss,
		    (getpagesize() * ru.ru_maxrss));
	}

	pprintf(p, "\nPlayer limit: %d\n", (max_connections - 10));
	pprintf(p, "\nThere are currently %d players, with a high of %d "
	    "since last restart.\n", player_count(1), player_high);
	pprintf(p, "There are currently %d games, with a high of %d "
	    "since last restart.\n", game_count(), game_high);
	pprintf(p, "\nCompiled on %s\n", COMP_DATE);

	return COM_OK;
}

PUBLIC int
com_date(int p, param_list param)
{
	time_t t = time(NULL);

	(void) param; // XXX: unused

	pprintf(p, "Local time     - %s\n", strltime(&t));
	pprintf(p, "Greenwich time - %s\n", strgtime(&t));

	return COM_OK;
}

PRIVATE int
plogins(int p, char *fname)
{
	FILE		*fp;
	char		 ipstr[20];
	char		 loginName[MAX_LOGIN_NAME + 1];
	int		 inout, registered;
	long int	 lval;
	time_t		 tval;

	if ((fp = fopen(fname, "r")) == NULL) {
		pprintf(p, "Sorry, no login information available.\n");
		return COM_OK;
	}

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

		tval = lval;

		pprintf(p, "%s: %-17s %-6s", strltime(&tval), loginName,
		    inout_string[inout]);

		if (parray[p].adminLevel > 0)
			pprintf(p, " from %s\n", ipstr);
		else
			pprintf(p, "\n");
	}

	fclose(fp);
	return COM_OK;
}

PUBLIC int
com_llogons(int p, param_list param)
{
	char fname[MAX_FILENAME_SIZE] = { '\0' };

	(void) param; // XXX: unused

	snprintf(fname, sizeof fname, "%s/%s", stats_dir, STATS_LOGONS);
	return plogins(p, fname);
}

PUBLIC int
com_logons(int p, param_list param)
{
	char fname[MAX_FILENAME_SIZE] = { '\0' };

	if (param[0].type == TYPE_WORD) {
		snprintf(fname, sizeof fname, "%s/player_data/%c/%s.%s",
		    stats_dir, param[0].val.word[0], param[0].val.word,
		    STATS_LOGONS);
	} else {
		snprintf(fname, sizeof fname, "%s/player_data/%c/%s.%s",
		    stats_dir, parray[p].login[0], parray[p].login,
		    STATS_LOGONS);
	}
	return plogins(p, fname);
}

PRIVATE void
AddPlayerLists(int p1, char *ptmp, const size_t size)
{
	if (parray[p1].adminLevel >= 10 && parray[p1].i_admin)
		strlcat(ptmp, "(*)", size);
	if (in_list(p1, L_COMPUTER, parray[p1].name))
		strlcat(ptmp, "(C)", size);
	if (in_list(p1, L_FM, parray[p1].name))
		strlcat(ptmp, "(FM)", size);
	if (in_list(p1, L_IM, parray[p1].name))
		strlcat(ptmp, "(IM)", size);
	if (in_list(p1, L_GM, parray[p1].name))
		strlcat(ptmp, "(GM)", size);
	if (in_list(p1, L_TD, parray[p1].name))
		strlcat(ptmp, "(TD)", size);
	if (in_list(p1, L_TEAMS, parray[p1].name))
		strlcat(ptmp, "(T)", size);
	if (in_list(p1, L_BLIND, parray[p1].name))
		strlcat(ptmp, "(B)", size);
}

PRIVATE void
who_terse(int p, int num, int *plist, int type)
{
	char		 ptmp[80 + 20] = { '\0' }; // for highlight
	int		 i;
	int		 p1;
	int		 rat;
	multicol	*m = multicol_start(PARRAY_SIZE);

	for (i = 0; i < num; i++) {
		p1 = plist[i];

		if (type == blitz_rat)
			rat = parray[p1].b_stats.rating;
		else if (type == wild_rat)
			rat = parray[p1].w_stats.rating;
		else if (type == std_rat)
			rat = parray[p1].s_stats.rating;
		else if (type == light_rat)
			rat = parray[p1].l_stats.rating;

		if (type == none) {
			strlcpy(ptmp, "     ", sizeof ptmp);
		} else {
			snprintf(ptmp, sizeof ptmp, "%-4s", ratstrii(rat,
			    parray[p1].registered));

			if (parray[p1].simul_info.numBoards) {
				strlcat(ptmp, "~", sizeof ptmp);
			} else if (parray[p1].game >= 0 &&
			    garray[parray[p1].game].status == GAME_EXAMINE) {
				strlcat(ptmp, "#", sizeof ptmp);
			} else if (parray[p1].game >= 0) {
				strlcat(ptmp, "^", sizeof ptmp);
			} else if (!parray[p1].open) {
				strlcat(ptmp, ":", sizeof ptmp);
			} else if (player_idle(p1) > 300) {
				strlcat(ptmp, ".", sizeof ptmp);
			} else {
				strlcat(ptmp, " ", sizeof ptmp);
			}
		}

		if (p == p1) {
			psprintf_highlight(p, ptmp + strlen(ptmp),
			    sizeof ptmp - strlen(ptmp), "%s", parray[p1].name);
		} else {
			strlcat(ptmp, parray[p1].name, sizeof ptmp);
		}

		AddPlayerLists(p1, ptmp, sizeof ptmp);
		multicol_store(m, ptmp);
	}

	multicol_pprint(m, p, 80, 2);
	multicol_end(m);

	pprintf(p, "\n %d players displayed (of %d). (*) indicates system "
	    "administrator.\n", num, player_count(1));
}

PRIVATE void
who_verbose(int p, int num, int plist[])
{
	char	 p1WithAttrs[255] = { '\0' };
	char	 playerLine[255] = { '\0' };
	char	 tmp[255] = { '\0' };
	int	 p1;
	int	 ret, too_long;

	pprintf(p, " +---------------------------------------------------------------+\n");
	pprintf(p, " |      User              Standard    Blitz        On for   Idle |\n");
	pprintf(p, " +---------------------------------------------------------------+\n");

	for (int i = 0; i < num; i++) {
		p1 = plist[i];
		strlcpy(playerLine, " |", sizeof playerLine);

		if (parray[p1].game >= 0)
			snprintf(tmp, sizeof tmp, "%3d", parray[p1].game + 1);
		else
			strlcpy(tmp, "   ", sizeof tmp);

		strlcat(playerLine, tmp, sizeof playerLine);

		if (!parray[p1].open)
			strlcpy(tmp, "X", sizeof tmp);
		else
			strlcpy(tmp, " ", sizeof tmp);

		strlcat(playerLine, tmp, sizeof playerLine);

		if (parray[p1].registered) {
			if (parray[p1].rated)
				strlcpy(tmp, " ", sizeof tmp);
			else
				strlcpy(tmp, "u", sizeof tmp);
		} else {
			strlcpy(tmp, "U", sizeof tmp);
		}

		strlcat(playerLine, tmp, sizeof playerLine);
		strlcpy(p1WithAttrs, parray[p1].name, sizeof p1WithAttrs);
		AddPlayerLists(p1, p1WithAttrs, sizeof p1WithAttrs);
		p1WithAttrs[17] = '\0';

		if (p == p1) {
			strlcpy(tmp, " ", sizeof tmp);
			psprintf_highlight(p, tmp + strlen(tmp),
			    sizeof tmp - strlen(tmp), "%-17s", p1WithAttrs);
		} else {
			ret = snprintf(tmp, sizeof tmp, " %-17s", p1WithAttrs);

			too_long = (ret < 0 || (size_t)ret >= sizeof tmp);

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

		strlcat(playerLine, tmp, sizeof playerLine);
		snprintf(tmp, sizeof tmp, " %4s        %-4s        %5s  ",
		    ratstrii(parray[p1].s_stats.rating,
		    parray[p1].registered),
		    ratstrii(parray[p1].b_stats.rating,
		    parray[p1].registered),
		    hms(player_ontime(p1), 0, 0, 0));
		strlcat(playerLine, tmp, sizeof playerLine);

		if (player_idle(p1) >= 60) {
			snprintf(tmp, sizeof tmp, "%5s   |\n",
			    hms(player_idle(p1), 0, 0, 0));
		} else {
			strlcpy(tmp, "        |\n", sizeof tmp);
		}

		strlcat(playerLine, tmp, sizeof playerLine);
		pprintf(p, "%s", playerLine);
	}

	pprintf(p, " |                                                               |\n");
	pprintf(p, " |    %3d Players Displayed                                      |\n", num);
	pprintf(p, " +---------------------------------------------------------------+\n");
}

PRIVATE void
who_winloss(int p, int num, int plist[])
{
	char	p1WithAttrs[255] = { '\0' };
	char	playerLine[255] = { '\0' };
	char	tmp[255] = { '\0' };
	int	p1;

	pprintf(p, "Name               Stand     win loss draw   Blitz    win loss draw    idle\n");
	pprintf(p, "----------------   -----     -------------   -----    -------------    ----\n");

	for (int i = 0; i < num; i++) {
		playerLine[0] = '\0';
		p1 = plist[i];

		/* Modified by hersco to include lists in 'who n.' */
		strlcpy(p1WithAttrs, parray[p1].name, sizeof p1WithAttrs);
		AddPlayerLists(p1, p1WithAttrs, sizeof p1WithAttrs);
		p1WithAttrs[17] = '\0';

		if (p1 == p) {
			psprintf_highlight(p, playerLine, sizeof playerLine,
			    "%-17s", p1WithAttrs);
		} else {
			snprintf(playerLine, sizeof playerLine, "%-17s",
			    p1WithAttrs);
		}

		snprintf(tmp, sizeof tmp, "  %4s     %4d %4d %4d   ",
		    ratstrii(parray[p1].s_stats.rating, parray[p1].registered),
		    parray[p1].s_stats.win,
		    parray[p1].s_stats.los,
		    parray[p1].s_stats.dra);
		strlcat(playerLine, tmp, sizeof playerLine);

		snprintf(tmp, sizeof tmp, "%4s    %4d %4d %4d   ",
		    ratstrii(parray[p1].b_stats.rating, parray[p1].registered),
		    parray[p1].b_stats.win,
		    parray[p1].b_stats.los,
		    parray[p1].b_stats.dra);
		strlcat(playerLine, tmp, sizeof playerLine);

		if (player_idle(p1) >= 60) {
			snprintf(tmp, sizeof tmp, "%5s\n", hms(player_idle(p1),
			    0, 0, 0));
		} else {
			strlcpy(tmp, "     \n", sizeof tmp);
		}

		strlcat(playerLine, tmp, sizeof playerLine);
		pprintf(p, "%s", playerLine);
	}

	pprintf(p, "    %3d Players Displayed.\n", num);
}

PRIVATE int
who_ok(int p, unsigned int sel_bits)
{
	int p2;

	if (parray[p].status != PLAYER_PROMPT)
		return 0;
	if (sel_bits == WHO_ALL)
		return 1;

	if (sel_bits & WHO_OPEN) {
		if (!parray[p].open)
			return 0;
	}
	if (sel_bits & WHO_CLOSED) {
		if (parray[p].open)
			return 0;
	}
	if (sel_bits & WHO_RATED) {
		if (!parray[p].rated)
			return 0;
	}
	if (sel_bits & WHO_UNRATED) {
		if (parray[p].rated)
			return 0;
	}
	if (sel_bits & WHO_FREE) {
		if (parray[p].game >= 0)
			return 0;
	}
	if (sel_bits & WHO_PLAYING) {
		if (parray[p].game < 0)
			return 0;
	}
	if (sel_bits & WHO_REGISTERED) {
		if (!parray[p].registered)
			return 0;
	}
	if (sel_bits & WHO_UNREGISTERED) {
		if (parray[p].registered)
			return 0;
	}
	if (sel_bits & WHO_BUGTEAM) {
		if ((p2 = parray[p].partner) < 0 || parray[p2].partner != p)
			return 0;
	}

	return 1;
}

PRIVATE int
blitz_cmp(const void *pp1, const void *pp2)
{
	register int	p1 = *(int *) pp1;
	register int	p2 = *(int *) pp2;

	if (parray[p1].status != PLAYER_PROMPT) {
		if (parray[p2].status != PLAYER_PROMPT)
			return 0;
		else
			return -1;
	}
	if (parray[p2].status != PLAYER_PROMPT)
		return 1;
	if (parray[p1].b_stats.rating > parray[p2].b_stats.rating)
		return -1;
	if (parray[p1].b_stats.rating < parray[p2].b_stats.rating)
		return 1;
	if (parray[p1].registered > parray[p2].registered)
		return -1;
	if (parray[p1].registered < parray[p2].registered)
		return 1;
	return strcmp(parray[p1].login, parray[p2].login);
}

PRIVATE int
light_cmp(const void *pp1, const void *pp2)
{
	register int	p1 = *(int *) pp1;
	register int	p2 = *(int *) pp2;

	if (parray[p1].status != PLAYER_PROMPT) {
		if (parray[p2].status != PLAYER_PROMPT)
			return 0;
		else
			return -1;
	}
	if (parray[p2].status != PLAYER_PROMPT)
		return 1;
	if (parray[p1].l_stats.rating > parray[p2].l_stats.rating)
		return -1;
	if (parray[p1].l_stats.rating < parray[p2].l_stats.rating)
		return 1;
	if (parray[p1].registered > parray[p2].registered)
		return -1;
	if (parray[p1].registered < parray[p2].registered)
		return 1;
	return strcmp(parray[p1].login, parray[p2].login);
}

PRIVATE int
stand_cmp(const void *pp1, const void *pp2)
{
	register int	p1 = *(int *) pp1;
	register int	p2 = *(int *) pp2;

	if (parray[p1].status != PLAYER_PROMPT) {
		if (parray[p2].status != PLAYER_PROMPT)
			return 0;
		else
			return -1;
	}
	if (parray[p2].status != PLAYER_PROMPT)
		return 1;
	if (parray[p1].s_stats.rating > parray[p2].s_stats.rating)
		return -1;
	if (parray[p1].s_stats.rating < parray[p2].s_stats.rating)
		return 1;
	if (parray[p1].registered > parray[p2].registered)
		return -1;
	if (parray[p1].registered < parray[p2].registered)
		return 1;
	return strcmp(parray[p1].login, parray[p2].login);
}

PRIVATE int
wild_cmp(const void *pp1, const void *pp2)
{
	register int	p1 = *(int *) pp1;
	register int	p2 = *(int *) pp2;

	if (parray[p1].status != PLAYER_PROMPT) {
		if (parray[p2].status != PLAYER_PROMPT)
			return 0;
		else
			return -1;
	}
	if (parray[p2].status != PLAYER_PROMPT)
		return 1;
	if (parray[p1].w_stats.rating > parray[p2].w_stats.rating)
		return -1;
	if (parray[p1].w_stats.rating < parray[p2].w_stats.rating)
		return 1;
	if (parray[p1].registered > parray[p2].registered)
		return -1;
	if (parray[p1].registered < parray[p2].registered)
		return 1;
	return strcmp(parray[p1].login, parray[p2].login);
}

PRIVATE int
alpha_cmp(const void *pp1, const void *pp2)
{
	register int	p1 = *(int *) pp1;
	register int	p2 = *(int *) pp2;

	if (parray[p1].status != PLAYER_PROMPT) {
		if (parray[p2].status != PLAYER_PROMPT)
			return 0;
		else
			return -1;
	}
	if (parray[p2].status != PLAYER_PROMPT)
		return 1;
	return strcmp(parray[p1].login, parray[p2].login);
}

PRIVATE void
sort_players(int players[PARRAY_SIZE], who_cmp_t cmp_func)
{
	if (p_num <= 0) {
		warnx("%s: p_num <= 0", __func__);
		return;
	}
	for (int i = 0; i < p_num; i++)
		players[i] = i;
	qsort(players, p_num, sizeof(int), cmp_func);
}

/*
 * This is the of the most compliclicated commands in terms of
 * parameters.
 */
PUBLIC int
com_who(int p, param_list param)
{
	char		 c;
	float		 start_perc = 0;
	float		 stop_perc = 1.0;
	int		 i, len;
	int		 p1, count, num_who;
	int		 plist[PARRAY_SIZE];
	int		 sort_type = blitz_rat;
	int		 sortlist[PARRAY_SIZE];
	int		 startpoint;
	int		 stoppoint;
	int		 style = 0;
	int		 tmpI, tmpJ;
	unsigned int	 sel_bits = WHO_ALL;
	who_cmp_t	 cmp_func = blitz_cmp;

	if (param[0].type != TYPE_NULL) {
		len = strlen(param[0].val.string);

		for (i = 0; i < len; i++) {
			c = param[0].val.string[i];

			if (isdigit(c)) {
				if (i == 0 || !isdigit(param[0].val.string[i - 1])) {
					tmpI = (c - '0');

					if (tmpI == 1) {
						start_perc = 0.0;
						stop_perc = 0.333333;
					} else if (tmpI == 2) {
						start_perc = 0.333333;
						stop_perc = 0.6666667;
					} else if (tmpI == 3) {
						start_perc = 0.6666667;
						stop_perc = 1.0;
					} else if (i == (len - 1) ||
					    !isdigit(param[0].val.string[i + 1]))
						return COM_BADPARAMETERS;
				} else {
					tmpI = c - '0';
					tmpJ = (param[0].val.string[i - 1] -
					    '0');

					if (tmpI == 0)
						return COM_BADPARAMETERS;
					if (tmpJ > tmpI)
						return COM_BADPARAMETERS;

					start_perc = ((float) tmpJ - 1.0) /
					    (float) tmpI;
					stop_perc = ((float) tmpJ) /
					    (float) tmpI;
				}
			} else {
				switch (c) {
				case ' ':
				case '\n':
				case '\t':
					break;
				case 'o':
					if (sel_bits == WHO_ALL)
						sel_bits = WHO_OPEN;
					else
						sel_bits |= WHO_OPEN;
					break;
				case 'r':
					if (sel_bits == WHO_ALL)
						sel_bits = WHO_RATED;
					else
						sel_bits |= WHO_RATED;
					break;
				case 'f':
					if (sel_bits == WHO_ALL)
						sel_bits = WHO_FREE;
					else
						sel_bits |= WHO_FREE;
					break;
				case 'a':
					if (sel_bits == WHO_ALL)
						sel_bits = WHO_FREE | WHO_OPEN;
					else
						sel_bits |= (WHO_FREE | WHO_OPEN);
					break;
				case 'R':
					if (sel_bits == WHO_ALL)
						sel_bits = WHO_REGISTERED;
					else
						sel_bits |= WHO_REGISTERED;
					break;
				case 'l':	// Sort order
					cmp_func	= alpha_cmp;
					sort_type	= none;
					break;
				case 'A':	// Sort order
					cmp_func	= alpha_cmp;
					sort_type	= none;
					break;
				case 'w':	// Sort order
					cmp_func	= wild_cmp;
					sort_type	= wild_rat;
					break;
				case 's':	// Sort order
					cmp_func	= stand_cmp;
					sort_type	= std_rat;
					break;
				case 'b':	// Sort order
					cmp_func	= blitz_cmp;
					sort_type	= blitz_rat;
					break;
				case 'L':	// Sort order
					cmp_func	= light_cmp;
					sort_type	= light_rat;
					break;
				case 't':	// format
					style = 0;
					break;
				case 'v':	// format
					style = 1;
					break;
				case 'n':	// format
					style = 2;
					break;
				case 'U':
					if (sel_bits == WHO_ALL)
						sel_bits = WHO_UNREGISTERED;
					else
						sel_bits |= WHO_UNREGISTERED;
					break;
				case 'B':
					if (sel_bits == WHO_ALL)
						sel_bits = WHO_BUGTEAM;
					else
						sel_bits |= WHO_BUGTEAM;
					break;
				default:
					return COM_BADPARAMETERS;
				}
			}
		}
	}

	sort_players(sortlist, cmp_func);
	count = 0;

	for (p1 = 0; p1 < p_num; p1++) {
		if (!who_ok(sortlist[p1], sel_bits))
			continue;
		count++;
	}

	startpoint	= floor((float) count * start_perc);
	stoppoint	= ceil((float) count * stop_perc) - 1;
	num_who		= 0;
	count		= 0;

	for (p1 = 0; p1 < p_num; p1++) {
		if (!who_ok(sortlist[p1], sel_bits))
			continue;
		if (count >= startpoint && count <= stoppoint)
			plist[num_who++] = sortlist[p1];

		count++;
	}

	if (num_who == 0) {
		pprintf(p, "No logged in players match the flags in your who "
		    "request.\n");
		return COM_OK;
	}

	switch (style) {
	case 0:		// terse
		who_terse(p, num_who, plist, sort_type);
		break;
	case 1:		// verbose
		who_verbose(p, num_who, plist);
		break;
	case 2:		// win-loss
		who_winloss(p, num_who, plist);
		break;
	default:
		return COM_BADPARAMETERS;
		break;
	}

	return COM_OK;
}

PUBLIC int
com_refresh(int p, param_list param)
{
	int g, p1;

	if (param[0].type == TYPE_NULL) {
		if (parray[p].game >= 0) {
			send_board_to(parray[p].game, p);
		} else { /* Do observing in here */
			if (parray[p].num_observe) {
				for (g = 0; g < parray[p].num_observe; g++) {
					send_board_to(parray[p].observe_list[g],
					    p);
				}
			} else {
				pprintf(p, "You are neither playing, observing "
				    "nor examining a game.\n");
				return COM_OK;
			}
		}
	} else {
		if ((g = GameNumFromParam(p, &p1, &param[0])) < 0)
			return COM_OK;
		if (g >= g_num ||
		    (garray[g].status != GAME_ACTIVE &&
		    garray[g].status != GAME_EXAMINE)) {
			pprintf(p, "No such game.\n");
		} else if (garray[g].private && parray[p].adminLevel ==
		    ADMIN_USER) {
			pprintf(p, "Sorry, game %d is a private game.\n",
			    (g + 1));
		} else {
			if (garray[g].private) {
				pprintf(p, "Refreshing PRIVATE game %d\n",
				    (g + 1));
			}
			send_board_to(g, p);
		}
	}

	return COM_OK;
}

PUBLIC int
com_prefresh(int p, param_list param)
{
	int retval, part;

	(void) param;

	if ((part = parray[p].partner) < 0) {
		pprintf(p, "You do not have a partner.\n");
		return COM_OK;
	} else if ((retval = pcommand(p, "refresh %s", parray[part].name)) ==
	    COM_OK)
		return COM_OK_NOPROMPT;
	return retval;
}

PUBLIC int
com_open(int p, param_list param)
{
	int retval;

	(void) param;
	ASSERT(param[0].type == TYPE_NULL);

	if ((retval = pcommand(p, "set open")) != COM_OK)
		return retval;
	return COM_OK_NOPROMPT;
}

PUBLIC int
com_simopen(int p, param_list param)
{
	int retval;

	(void) param;
	ASSERT(param[0].type == TYPE_NULL);

	if ((retval = pcommand(p, "set simopen")) != COM_OK)
		return retval;
	return COM_OK_NOPROMPT;
}

PUBLIC int
com_bell(int p, param_list param)
{
	int retval;

	(void) param;
	ASSERT(param[0].type == TYPE_NULL);

	if ((retval = pcommand(p, "set bell")) != COM_OK)
		return retval;
	return COM_OK_NOPROMPT;
}

PUBLIC int
com_flip(int p, param_list param)
{
	int retval;

	(void) param;
	ASSERT(param[0].type == TYPE_NULL);

	if ((retval = pcommand(p, "set flip")) != COM_OK)
		return retval;
	return COM_OK_NOPROMPT;
}

PUBLIC int
com_highlight(int p, param_list param)
{
	// XXX: unused
	(void) param;

	pprintf(p, "Obsolete command. Please do set highlight <0-15>.\n");
	return COM_OK;
}

PUBLIC int
com_style(int p, param_list param)
{
	int retval;

	ASSERT(param[0].type == TYPE_INT);

	if ((retval = pcommand(p, "set style %d", param[0].val.integer)) !=
	    COM_OK)
		return retval;
	return COM_OK_NOPROMPT;
}

PUBLIC int
com_promote(int p, param_list param)
{
	int retval;

	ASSERT(param[0].type == TYPE_WORD);

	if ((retval = pcommand(p, "set promote %s", param[0].val.word)) !=
	    COM_OK)
		return retval;
	return COM_OK_NOPROMPT;
}

PUBLIC int
com_alias(int p, param_list param)
{
	int al;

	if (param[0].type == TYPE_NULL) {
		for (al = 0; al < parray[p].numAlias; al++) {
			pprintf(p, "%s -> %s\n",
			    parray[p].alias_list[al].comm_name,
			    parray[p].alias_list[al].alias);
		}

		return COM_OK;
	}

	al = alias_lookup(param[0].val.word, parray[p].alias_list,
	    parray[p].numAlias);

	if (param[1].type == TYPE_NULL) {
		if (al < 0) {
			pprintf(p, "You have no alias named '%s'.\n",
			    param[0].val.word);
		} else {
			pprintf(p, "%s -> %s\n",
			    parray[p].alias_list[al].comm_name,
			    parray[p].alias_list[al].alias);
		}
	} else {
		if (al < 0) {
			if (parray[p].numAlias >= MAX_ALIASES - 1) {
				pprintf(p, "You have your maximum of %d "
				    "aliases.\n", (MAX_ALIASES - 1));
			} else {
				if (!strcmp(param[0].val.string, "quit")) {
					pprintf(p, "You can't alias this "
					    "command.\n");
				} else if (!strcmp(param[0].val.string,
				    "unalias")) {
					pprintf(p, "You can't alias this "
					    "command.\n");
				} else {
					parray[p].alias_list[parray[p].numAlias].comm_name =
					    xstrdup(param[0].val.word);
					parray[p].alias_list[parray[p].numAlias].alias =
					    xstrdup(param[1].val.string);
					parray[p].numAlias++;
					pprintf(p, "Alias set.\n");
				}
			}
		} else {
			rfree(parray[p].alias_list[al].alias);
			parray[p].alias_list[al].alias =
			    xstrdup(param[1].val.string);
			pprintf(p, "Alias replaced.\n");
		}

		parray[p].alias_list[parray[p].numAlias].comm_name = NULL;
	}

	return COM_OK;
}

PUBLIC int
com_unalias(int p, param_list param)
{
	int al;

	ASSERT(param[0].type == TYPE_WORD);

	al = alias_lookup(param[0].val.word, parray[p].alias_list,
	    parray[p].numAlias);

	if (al < 0) {
		pprintf(p, "You have no alias named '%s'.\n",
		    param[0].val.word);
	} else {
		rfree(parray[p].alias_list[al].comm_name);
		rfree(parray[p].alias_list[al].alias);

		for (int i = al; i < parray[p].numAlias; i++) {
			parray[p].alias_list[i].comm_name =
			    parray[p].alias_list[i + 1].comm_name;
			parray[p].alias_list[i].alias =
			    parray[p].alias_list[i + 1].alias;
		}

		parray[p].numAlias--;
		parray[p].alias_list[parray[p].numAlias].comm_name = NULL;
		pprintf(p, "Alias removed.\n");
	}

	return COM_OK;
}

PUBLIC int
com_servers(int p, param_list param)
{
	pprintf(p, "There are no other servers known to this server.\n");
	(void) param;
	return COM_OK;
}

PUBLIC int
com_index(int p, param_list param)
{
	char	*iwant, *filenames[1000];
	char	 index_default[] = "_index";
	int	 i;

	if (param[0].type == TYPE_NULL) {
		iwant = index_default;
	} else {
		iwant = param[0].val.word;

		if (!safestring(iwant)) {
			pprintf(p, "Illegal character in category %s.\n",
			    iwant);
			return COM_OK;
		}
	}

	i = search_directory(index_dir, iwant, filenames,
	    ARRAY_SIZE(filenames));

	if (i == 0) {
		pprintf(p, "No index entry for \"%s\".\n", iwant);
	} else if (i == 1 || !strcmp(*filenames, iwant)) {
		if (psend_file(p, index_dir, *filenames)) {
			/*
			 * We should never reach this unless the file
			 * was just deleted.
			 */
			pprintf(p, "Index file %s could not be found! ",
			    *filenames);
			pprintf(p, "Please inform an admin of this. "
			    "Thank you.\n");
		}
	} else {
		pprintf(p, "Matches:");
		display_directory(p, filenames, i);
	}

	return COM_OK;
}

PUBLIC int
com_help(int p, param_list param)
{
	char	*iwant, *filenames[1000];
	char	 help_default[] = "_help";
	int	 i, UseLang = parray[p].language;

	if (param[0].type == TYPE_NULL) {
		iwant = help_default;
	} else {
		iwant = param[0].val.word;

		if (!safestring(iwant)) {
			pprintf(p, "Illegal character in command %s.\n", iwant);
			return COM_OK;
		}
	}

	i = search_directory(help_dir[UseLang], iwant, filenames,
	    ARRAY_SIZE(filenames));

	if (i == 0) {
		if (UseLang != LANG_DEFAULT) {
			i += search_directory(help_dir[LANG_DEFAULT], iwant,
			    filenames, ARRAY_SIZE(filenames));

			if (i > 0) {
				pprintf(p, "No help available in %s; using %s "
				    "instead.\n",
				    Language(UseLang),
				    Language(LANG_DEFAULT));
				UseLang = LANG_DEFAULT;
			}
		}

		if (i == 0) {
			pprintf(p, "No help available on \"%s\".\n", iwant);
			return COM_OK;
		}
	}

	if (i == 1 || !strcmp(*filenames, iwant)) {
		if (psend_file(p, help_dir[UseLang], *filenames)) {
			/*
			 * We should never reach this unless the file
			 * was just deleted.
			 */
			pprintf(p, "Helpfile %s could not be found! ",
			    *filenames);
			pprintf(p, "Please inform an admin of this. "
			    "Thank you.\n");
		}
	} else {
		pprintf(p, "Matches:");
		display_directory(p, filenames, i);
		pprintf(p, "[Type \"info\" for a list of FICS general "
		    "information files.]\n");
	}

	return COM_OK;
}

PUBLIC int
com_info(int p, param_list param)
{
	char	*filenames[1000];
	int	 n;

	// XXX: unused
	(void) param;

	if ((n = search_directory(info_dir, NULL, filenames,
	    ARRAY_SIZE(filenames))) > 0)
		display_directory(p, filenames, n);
	return COM_OK;
}

PRIVATE int
FindAndShowFile(int p, param_list param, char *dir)
{
	char		*iwant, *filenames[1000];
	int		 i;
	static char	 nullify = '\0';

	if (param[0].type == TYPE_NULL) {
		iwant = &nullify;
	} else {
		iwant = param[0].val.word;

		if (!safestring(iwant)) {
			pprintf(p, "Illegal character in filename %s.\n",
			    iwant);
			return COM_OK;
		}
	}

	i = search_directory(dir, iwant, filenames, ARRAY_SIZE(filenames));

	if (i == 0) {
		pprintf(p, "No information available on \"%s\".\n", iwant);
	} else if (i == 1 || !strcmp(*filenames, iwant)) {
		if (psend_file(p, dir, *filenames)) {
			/*
			 * We should never reach this unless the file
			 * was just deleted.
			 */
			pprintf(p, "File %s could not be found! ", *filenames);
			pprintf(p, "Please inform an admin of this. "
			    "Thank you.\n");
		}
	} else {
		if (*iwant)
			pprintf(p, "Matches:\n");
		display_directory(p, filenames, i);
	}

	return COM_OK;
}

PUBLIC int
com_uscf(int p, param_list param)
{
	return FindAndShowFile(p, param, uscf_dir);
}

PUBLIC int
com_adhelp(int p, param_list param)
{
	return FindAndShowFile(p, param, adhelp_dir);
}

PUBLIC int
com_mailsource(int p, param_list param)
{
	char		*buffer[1000];
	char		*iwant;
	char		 fname[MAX_FILENAME_SIZE];
	char		 subj[120];
	int		 count;
	static char	 nullify = '\0';

	if (!parray[p].registered) {
		pprintf(p, "Only registered people can use the mailsource "
		    "command.\n");
		return COM_OK;
	}

	if (param[0].type == TYPE_NULL)
		iwant = &nullify;
	else
		iwant = param[0].val.word;

	if ((count = search_directory(source_dir, iwant, buffer,
	    ARRAY_SIZE(buffer))) == 0) {
		pprintf(p, "Found no source file matching \"%s\".\n", iwant);
	} else if ((count == 1) || !strcmp(iwant, *buffer)) {
		snprintf(subj, sizeof subj, "FICS source file from server "
		    "%s: %s",
		    fics_hostname,
		    *buffer);
		snprintf(fname, sizeof fname, "%s/%s",
		    source_dir,
		    *buffer);

		mail_file_to_user(p, subj, fname);

		pprintf(p, "Source file %s sent to %s\n", *buffer,
		    parray[p].emailAddress);
	} else {
		pprintf(p, "Found %d source files matching that:\n", count);

		if (*iwant) {
			display_directory(p, buffer, count);
		} else { // this junk is to get *.c *.h
			char		*s;
			multicol	*m = multicol_start(count);

			for (int i = 0; i < count; i++) {
				s = (buffer[i] + strlen(buffer[i]) - 2);

				if (s >= buffer[i] &&
				    (!strcmp(s, ".c") || !strcmp(s, ".h")))
					multicol_store(m, buffer[i]);
			}

			multicol_pprint(m, p, 78, 1);
			multicol_end(m);
		}
	}

	return COM_OK;
}

PUBLIC int
com_mailhelp(int p, param_list param)
{
	char		*buffer[1000];
	char		*iwant;
	char		 fname[MAX_FILENAME_SIZE];
	char		 subj[120];
	int		 count;
	int		 lang = parray[p].language;
	static char	 nullify = '\0';

	if (!parray[p].registered) {
		pprintf(p, "Only registered people can use the mailhelp "
		    "command.\n");
		return COM_OK;
	}

	if (param[0].type == TYPE_NULL)
		iwant = &nullify;
	else
		iwant = param[0].val.word;

	count = search_directory(help_dir[lang], iwant, buffer,
	    ARRAY_SIZE(buffer));

	if (count == 0 && lang != LANG_DEFAULT) {
		count += search_directory(help_dir[LANG_DEFAULT], iwant, buffer,
		    ARRAY_SIZE(buffer));

		if (count > 0) {
			pprintf(p, "No help available in %s; "
			    "using %s instead.\n",
			    Language(lang),
			    Language(LANG_DEFAULT));
			lang = LANG_DEFAULT;
		}
	}

	if (count == 0) {
		pprintf(p, "Found no help file matching \"%s\".\n", iwant);
	} else if (count == 1 || !strcmp(*buffer, iwant)) {
		snprintf(subj, sizeof subj, "FICS help file from server %s: %s",
		    fics_hostname,
		    *buffer);
		snprintf(fname, sizeof fname, "%s/%s",
		    help_dir[lang],
		    *buffer);

		mail_file_to_user(p, subj, fname);

		pprintf(p, "Help file %s sent to %s\n", *buffer,
		    parray[p].emailAddress);
	} else {
		pprintf(p, "Found %d helpfiles matching that:\n", count);
		display_directory(p, buffer, count);
	}

	return COM_OK;
}

PUBLIC int
com_handles(int p, param_list param)
{
	char	*buffer[1000];
	char	 pdir[MAX_FILENAME_SIZE];
	int	 count;

	snprintf(pdir, sizeof pdir, "%s/%c", player_dir, param[0].val.word[0]);

	count = search_directory(pdir, param[0].val.word, buffer,
	    ARRAY_SIZE(buffer));
	pprintf(p, "Found %d names.\n", count);

	if (count > 0)
		display_directory(p, buffer, count);

	return COM_OK;
}

PUBLIC int
com_getpi(int p, param_list param)
{
	int p1;

	if (!in_list(p, L_TD, parray[p].name)) {
		pprintf(p, "Only TD programs are allowed to use this command."
		    "\n");
		return COM_OK;
	}

	if ((p1 = player_find_bylogin(param[0].val.word)) < 0 ||
	    parray[p1].registered == 0)
		return COM_OK;

	if (!parray[p1].registered) {
		pprintf(p, "*getpi %s -1 -1 -1*\n", parray[p1].name);
	} else {
		pprintf(p, "*getpi %s %d %d %d*\n", parray[p1].name,
		    parray[p1].w_stats.rating,
		    parray[p1].b_stats.rating,
		    parray[p1].s_stats.rating);
	}

	return COM_OK;
}

PUBLIC int
com_getps(int p, param_list param)
{
	int p1;

	if (((p1 = player_find_bylogin(param[0].val.word)) < 0 ||
	    parray[p1].registered == 0) ||
	    parray[p1].game < 0) {
		pprintf(p, "*status %s 1*\n", param[0].val.word);
		return COM_OK;
	}

	pprintf(p, "*status %s 0 %s*\n", parray[p1].name,
	    parray[(parray[p1].opponent)].name);
	return COM_OK;
}

PUBLIC int
com_limits(int p, param_list param)
{
	// XXX: unused
	(void) param;

	pprintf(p, "\nCurrent hardcoded limits:\n");
	pprintf(p, "  Max number of players: %d\n", MAX_PLAYER);
	pprintf(p, "  Max number of channels and max capacity: %d\n",
	    MAX_CHANNELS);
	pprintf(p, "  Max number of channels one can be in: %d\n",
	    MAX_INCHANNELS);
	pprintf(p, "  Max number of people on the notify list: %d\n",
	    MAX_NOTIFY);
	pprintf(p, "  Max number of aliases: %d\n", (MAX_ALIASES - 1));
	pprintf(p, "  Max number of games you can observe at a time: %d\n",
	    MAX_OBSERVE);
	pprintf(p, "  Max number of requests pending: %d\n", MAX_PENDING);
	pprintf(p, "  Max number of people on the censor list: %d\n",
	    MAX_CENSOR);
	pprintf(p, "  Max number of people in a simul game: %d\n", MAX_SIMUL);
	pprintf(p, "  Max number of messages one can receive: %d\n",
	    MAX_MESSAGES);
	pprintf(p, "  Min number of games to be active: %d\n", PROVISIONAL);

	if (parray[p].adminLevel < ADMIN_ADMIN &&
	    !titled_player(p, parray[p].login)) {
		pprintf(p, "  Size of journal (entries): %d\n", MAX_JOURNAL);
	} else {
		pprintf(p, "  Size of journal (entries): 26\n");
	}

	pprintf(p, "\nAdmin settable limits:\n");
	pprintf(p, "  Shout quota gives two shouts per %d seconds.\n",
	    quota_time);

	return COM_OK;
}