/* obsproc.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
   Dave Herscovici		95/11/26	Created
   Markus Uhlin			23/12/13	Fixed bugs
   Markus Uhlin			23/12/13	Reformatted functions
   Markus Uhlin			24/04/28	Completed reformatting
   Markus Uhlin			24/04/28	Replaced unbounded string
						handling functions.
   Markus Uhlin			24/04/29	Fixed compiler warnings
   Markus Uhlin			24/07/07	Fixed unhandled return values of
						fscanf().
*/

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

#include <err.h>

#include "command.h"
#include "comproc.h"
#include "config.h"
#include "eco.h"
#include "ficsmain.h"
#include "formula.h"
#include "gamedb.h"
#include "gameproc.h"
#include "matchproc.h"
#include "maxxes-utils.h"
#include "movecheck.h"
#include "network.h"
#include "obsproc.h"
#include "playerdb.h"
#include "ratings.h"
#include "rmalloc.h"
#include "utils.h"

PUBLIC int
GameNumFromParam(int p, int *p1, parameter *param)
{
	if (param->type == TYPE_WORD) {
		*p1 = player_find_part_login(param->val.word);

		if (*p1 < 0) {
			pprintf(p, "No user named \"%s\" is logged in.\n",
			    param->val.word);
			return -1;
		}

		if (parray[*p1].game < 0) {
			pprintf(p, "%s is not playing a game.\n",
			    parray[*p1].name);
		}

		return parray[*p1].game;
	} else { // Must be an integer
		*p1 = -1;

		if (param->val.integer <= 0) {
			pprintf(p, "%d is not a valid game number.\n",
			    param->val.integer);
		}

		return (param->val.integer - 1);
	}
}

PRIVATE int
gamesortfunc(const void *i, const void *j)
{
	/*
	 * examine mode games moved to top of "games" output
	 */
	return (GetRating(&parray[garray[*(int *)i].white],
	    garray[*(int *)i].type) +
	    GetRating(&parray[garray[*(int *)i].black],
	    garray[*(int *)i].type) -
	    (garray[*(int *)i].status == GAME_EXAMINE ? 10000 : 0) -
	    GetRating(&parray[garray[*(int *)j].white],
	    garray[*(int *)j].type) -
	    GetRating(&parray[garray[*(int *)j].black],
	    garray[*(int *)j].type) +
	    (garray[*(int *)j].status == GAME_EXAMINE ? 10000 : 0));
}

PUBLIC int
com_games(int p, param_list param)
{
	char	*s = NULL;
	int	*sortedgames;	// for qsort
	int	 count = 0;
	int	 i, j;
	int	 selected = 0;
	int	 slen = 0;
	int	 totalcount;
	int	 wp, bp;
	int	 ws, bs;

	totalcount = game_count();

	if (totalcount == 0) {
		pprintf(p, "There are no games in progress.\n");
	} else {
		// for qsort
		sortedgames = reallocarray(NULL, totalcount, sizeof(int));

		if (sortedgames == NULL)
			err(1, "%s", __func__);
		else
			malloc_count++;

		if (param[0].type == TYPE_WORD) {
			s	= param[0].val.word;
			slen	= strlen(s);

			if ((selected = atoi(s)) < 0)
				selected = 0;
		}

		for (i = 0; i < g_num; i++) {
			if (garray[i].status != GAME_ACTIVE &&
			    garray[i].status != GAME_EXAMINE)
				continue;
			if ((selected) && (selected != i + 1))
				continue;	// not selected game number

			wp	= garray[i].white;
			bp	= garray[i].black;

			if ((!selected) &&
			    s &&
			    strncasecmp(s, garray[i].white_name, slen) &&
			    strncasecmp(s, garray[i].black_name, slen))
				continue;	// player names did not match
			sortedgames[count++] = i;
		} /* for */

		if (!count) {
			pprintf(p, "No matching games were found "
			    "(of %d in progress).\n", totalcount);
		} else {
			qsort(sortedgames, count, sizeof(int), gamesortfunc);
			pprintf(p, "\n");

			for (j = 0; j < count; j++) {
				i = sortedgames[j];

				wp	= garray[i].white;
				bp	= garray[i].black;

				board_calc_strength(&garray[i].game_state,
				    &ws, &bs);

				if (garray[i].status != GAME_EXAMINE) {
					pprintf_noformat(p, "%2d %4s %-11.11s "
					    "%4s %-10.10s [%c%c%c%3d %3d] ",
					    (i + 1),
					    ratstrii(GetRating(&parray[wp],
					    garray[i].type),
					    parray[wp].registered),
					    parray[wp].name,
					    ratstrii(GetRating(&parray[bp],
					    garray[i].type),
					    parray[bp].registered),
					    parray[bp].name,
					    (garray[i].private ? 'p' : ' '),
					    *bstr[garray[i].type],
					    *rstr[garray[i].rated],
					    (garray[i].wInitTime / 600),
					    (garray[i].wIncrement / 10));

					game_update_time(i);

					pprintf_noformat(p, "%6s -",
					    tenth_str((garray[i].wTime > 0 ?
					    garray[i].wTime : 0), 0));

					pprintf_noformat(p, "%6s (%2d-%2d) "
					    "%c: %2d\n",
					    tenth_str((garray[i].bTime > 0 ?
					    garray[i].bTime : 0), 0),
					    ws,
					    bs,
					    (garray[i].game_state.onMove ==
					    WHITE ? 'W' : 'B'),
					    garray[i].game_state.moveNum);
				} else {
					pprintf_noformat(p, "%2d "
					    "(Exam. %4d %-11.11s %4d %-10.10s) "
					    "[%c%c%c%3d %3d] ",
					    (i + 1),
					    garray[i].white_rating,
					    garray[i].white_name,
					    garray[i].black_rating,
					    garray[i].black_name,
					    (garray[i].private ? 'p' : ' '),
					    *bstr[garray[i].type],
					    *rstr[garray[i].rated],
					    (garray[i].wInitTime / 600),
					    (garray[i].wIncrement / 10));

					pprintf_noformat(p, "%c: %2d\n",
					    (garray[i].game_state.onMove ==
					    WHITE ? 'W' : 'B'),
					    garray[i].game_state.moveNum);
				}
			} /* for */

			if (count < totalcount) {
				pprintf(p, "\n  %d game%s displayed "
				    "(of %d in progress).\n",
				    count,
				    (count == 1 ? "" : "s"),
				    totalcount);
			} else {
				pprintf(p, "\n  %d game%s displayed.\n",
				    totalcount, (totalcount == 1 ? "" : "s"));
			}
		} /* else */

		rfree(sortedgames);
	}

	return COM_OK;
}

PRIVATE int
do_observe(int p, int obgame)
{
	if (garray[obgame].private &&
	    parray[p].adminLevel < ADMIN_ADMIN) {
		pprintf(p, "Sorry, game %d is a private game.\n", (obgame + 1));
		return COM_OK;
	}

	if (garray[obgame].white == p ||
	    garray[obgame].black == p) {
		if (garray[obgame].status != GAME_EXAMINE) {
			pprintf(p, "You cannot observe a game that you are "
			    "playing.\n");
			return COM_OK;
		}
	}

	if (player_is_observe(p, obgame)) {
		pprintf(p, "Removing game %d from observation list.\n",
		    (obgame + 1));
		player_remove_observe(p, obgame);
	} else {
		if (!player_add_observe(p, obgame)) {
			pprintf(p, "You are now observing game %d.\n",
			    (obgame + 1));
			send_board_to(obgame, p);
		} else {
			pprintf(p, "You are already observing the maximum "
			    "number of games.\n");
		}
	}

	return COM_OK;
}

PUBLIC void
unobserveAll(int p)
{
	for (int i = 0; i < parray[p].num_observe; i++) {
		pprintf(p, "Removing game %d from observation list.\n",
		    (parray[p].observe_list[i] + 1));
	}

	parray[p].num_observe = 0;
}

PUBLIC int
com_unobserve(int p, param_list param)
{
	int	gNum, p1;

	if (param[0].type == TYPE_NULL) {
		unobserveAll(p);
		return COM_OK;
	}

	if ((gNum = GameNumFromParam(p, &p1, &param[0])) < 0)
		return COM_OK;

	if (!player_is_observe(p, gNum)) {
		pprintf(p, "You are not observing game %d.\n", gNum);
	} else {
		player_remove_observe(p, gNum);
		pprintf(p, "Removing game %d from observation list.\n",
		    (gNum + 1));
	}

	return COM_OK;
}

PUBLIC int
com_observe(int p, param_list param)
{
	int	i;
	int	p1, obgame;

	if (parray[p].game >= 0 && garray[parray[p].game].status ==
	    GAME_EXAMINE) {
		pprintf(p, "You are still examining a game.\n");
		return COM_OK;
	}

	if (param[0].type == TYPE_NULL) {
		unobserveAll(p);
		return COM_OK;
	}

	if ((obgame = GameNumFromParam(p, &p1, &param[0])) < 0)
		return COM_OK;

	if ((obgame >= g_num) ||
	    ((garray[obgame].status != GAME_ACTIVE) &&
	    (garray[obgame].status != GAME_EXAMINE))) {
		pprintf(p, "There is no such game.\n");
		return COM_OK;
	}

	if (p1 >= 0 && parray[p1].simul_info.numBoards) {
		for (i = 0; i < parray[p1].simul_info.numBoards; i++)
			if (parray[p1].simul_info.boards[i] >= 0)
				do_observe(p, parray[p1].simul_info.boards[i]);
	} else {
		do_observe(p, obgame);
	}

	return COM_OK;
}

PUBLIC int
com_allobservers(int p, param_list param)
{
	int	first;
	int	g;
	int	obgame;
	int	p1;
	int	start, end;

	if (param[0].type == TYPE_NULL) {
		obgame = -1;
	} else {
		obgame = GameNumFromParam(p, &p1, &param[0]);

		if (obgame < 0)
			return COM_OK;
	}

	if (obgame == -1) {
		start = 0;
		end = g_num;
	} else if ((obgame >= g_num) ||
	    ((obgame < g_num) &&
	    ((garray[obgame].status != GAME_ACTIVE) &&
	    (garray[obgame].status != GAME_EXAMINE)))) {
		pprintf(p, "There is no such game.\n");
		return COM_OK;
	} else {
		start = obgame;
		end = obgame + 1;
	}

	/*
	 * list games being played
	 */
	for (g = start; g < end; g++) {
		if ((garray[g].status == GAME_ACTIVE) &&
		    ((parray[p].adminLevel > 0) ||
		    (garray[g].private == 0))) {
			for (first = 1, p1 = 0; p1 < p_num; p1++) {
				if (parray[p1].status != PLAYER_EMPTY &&
				    player_is_observe(p1, g)) {
					if (first) {
						pprintf(p, "Observing %2d "
						    "[%s vs. %s]:",
						    (g + 1),
						    parray[garray[g].white].name,
						    parray[garray[g].black].name);
						first = 0;
					}

					pprintf(p, " %s%s",
					    (parray[p1].game >= 0 ? "#" : ""),
					    parray[p1].name);
				}
			}

			if (!first)
				pprintf(p, "\n");
		}
	} /* for */

	/*
	 * list games being examined last
	 */
	for (g = start; g < end; g++) {
		if ((garray[g].status == GAME_EXAMINE) &&
		    ((parray[p].adminLevel > 0) ||
		    (garray[g].private == 0))) {
			for (first = 1, p1 = 0; p1 < p_num; p1++) {
				if ((parray[p1].status != PLAYER_EMPTY) &&
				    (player_is_observe(p1, g) ||
				    (parray[p1].game == g))) {
					if (first) {
						if (strcmp(garray[g].white_name,
						    garray[g].black_name)) {
							pprintf(p, "Examining "
							    "%2d [%s vs %s]:",
							    (g + 1),
							    garray[g].white_name,
							    garray[g].black_name);
						} else {
							pprintf(p, "Examining "
							    "%2d (scratch):",
							    (g + 1));
						}

						first = 0;
					}

					pprintf(p, " %s%s",
					    (parray[p1].game == g ? "#" : ""),
					    parray[p1].name);
				}
			}

			if (!first)
				pprintf(p, "\n");
		}
	} /* for */

	return COM_OK;
}

PUBLIC int
com_unexamine(int p, param_list param)
{
	int	g, p1, flag = 0;

	if (parray[p].game < 0 || garray[parray[p].game].status !=
	    GAME_EXAMINE) {
		pprintf(p, "You are not examining any games.\n");
		return COM_OK;
	}

	g = parray[p].game;
	parray[p].game = -1;

	for (p1 = 0; p1 < p_num; p1++) {
		if (parray[p1].status != PLAYER_PROMPT)
			continue;

		if (parray[p1].game == g && p != p1) {
			/*
			 * ok - there are other examiners to take over
			 * the game.
			 */
			flag = 1;
		}

		if (player_is_observe(p1, g) || parray[p1].game == g) {
			pprintf(p1, "%s stopped examining game %d.\n",
			    parray[p].name, (g + 1));
		}
	}

	if (!flag) {
		for (p1 = 0; p1 < p_num; p1++) {
			if (parray[p1].status != PLAYER_PROMPT)
				continue;
			if (player_is_observe(p1, g)) {
				pprintf(p1, "There are no examiners.\n");
				pcommand(p1, "unobserve %d", (g + 1));
			}
		}

		game_remove(g);
	}

	pprintf(p, "You are no longer examining game %d.\n", (g + 1));
	return COM_OK;
}

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

	if (parray[p].game < 0 || garray[parray[p].game].status !=
	    GAME_EXAMINE) {
		pprintf(p, "You are not examining any games.\n");
		return COM_OK;
	}

	if ((p1 = player_find_part_login(param[0].val.word)) < 0) {
		pprintf(p, "No user named \"%s\" is logged in.\n",
		    param[0].val.word);
		return COM_OK;
	}

	g = parray[p].game;

	if (!player_is_observe(p1, g)) {
		pprintf(p, "%s must observe the game you are analysing.\n",
		    parray[p1].name);
		return COM_OK;
	} else {
		if (parray[p1].game >= 0) {
			pprintf(p, "%s is already analysing the game.\n",
			    parray[p1].name);
			return COM_OK;
		}

		/*
		 * If we get here - let's make him examiner of the
		 * game.
		 */
		unobserveAll(p1);	// Fix for Xboard

		player_decline_offers(p1, -1, PEND_MATCH);
		player_withdraw_offers(p1, -1, PEND_MATCH);
		player_withdraw_offers(p1, -1, PEND_SIMUL);

		parray[p1].game = g;
		pprintf(p1, "You are now examiner of game %d.\n", (g + 1));
		send_board_to(g, p1);	// Pos not changed - but fixes Xboard

		for (p2 = 0; p2 < p_num; p2++) {
			if (parray[p2].status != PLAYER_PROMPT)
				continue;
			if (p2 == p1)
				continue;
			if (player_is_observe(p2, g) || parray[p2].game == g) {
				pprintf_prompt(p2, "%s is now examiner of "
				    "game %d.\n", parray[p1].name, (g + 1));
			}
		}
	}

	return COM_OK;
}

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

	if (param[0].type == TYPE_NULL) {
		if (parray[p].game >= 0) {
			g = parray[p].game;
		} else if (parray[p].num_observe) {
			for (g = 0; g < parray[p].num_observe; g++) {
				pprintf(p, "%s\n",
				    movesToString(parray[p].observe_list[g],
				    0));
			}

			return COM_OK;
		} else {
			pprintf(p, "You are neither playing, observing nor "
			    "examining a game.\n");
			return COM_OK;
		}
	} else {
		g = GameNumFromParam(p, &p1, &param[0]);

		if (g < 0)
			return COM_OK;
	}

	if ((g < 0) ||
	    (g >= g_num) ||
	    ((garray[g].status != GAME_ACTIVE) &&
	    (garray[g].status != GAME_EXAMINE))) {
		pprintf(p, "There is no such game.\n");
		return COM_OK;
	}

	if ((garray[g].white != p) &&
	    (garray[g].black != p) &&
	    (garray[g].private) &&
	    (parray[p].adminLevel < ADMIN_ADMIN)) {
		pprintf(p, "Sorry, that is a private game.\n");
		return COM_OK;
	}

	pprintf(p, "%s\n", movesToString(g, 0)); // pgn may break interfaces?
	return COM_OK;
}

PUBLIC int
com_mailmoves(int p, param_list param)
{
	char	subj[81] = { '\0' };
	int	g;
	int	p1;

	if (!parray[p].registered) {
		pprintf(p, "Unregistered players cannot use mailmoves.\n");
		return COM_OK;
	}

	if (param[0].type == TYPE_NULL) {
		if (parray[p].game >= 0) {
			g = parray[p].game;
		} else {
			pprintf(p, "You are neither playing, observing nor "
			    "examining a game.\n");
			return COM_OK;
		}
	} else {
		g = GameNumFromParam(p, &p1, &param[0]);

		if (g < 0)
			return COM_OK;
	}

	if ((g < 0) ||
	    (g >= g_num) ||
	    ((garray[g].status != GAME_ACTIVE) &&
	    (garray[g].status != GAME_EXAMINE))) {
		pprintf(p, "There is no such game.\n");
		return COM_OK;
	}

	if ((garray[g].white != p) &&
	    (garray[g].black != p) &&
	    (garray[g].private) &&
	    (parray[p].adminLevel < ADMIN_ADMIN)) {
		pprintf(p, "Sorry, that is a private game.\n");
		return COM_OK;
	}

	msnprintf(subj, sizeof subj, "FICS game report %s vs %s",
	    garray[g].white_name,
	    garray[g].black_name);

	if (mail_string_to_user(p, subj, movesToString(g, parray[p].pgn))) {
		pprintf(p, "Moves NOT mailed, perhaps your address is "
		    "incorrect.\n");
	} else {
		pprintf(p, "Moves mailed.\n");
	}

	return COM_OK;
}

PRIVATE int
old_mail_moves(int p, int mail, param_list param)
{
	FILE	*fp;
	char	 fname[MAX_FILENAME_SIZE] = { '\0' };
	char	 tmp[2048] = { '\0' };
	char	*ptmp = tmp;
	int	 count;
	int	 p1, connected;

	if (mail && (!parray[p].registered)) {
		pprintf(p, "Unregistered players cannot use mailoldmoves.\n");
		return COM_OK;
	}

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

	(void) snprintf(fname, sizeof fname, "%s/player_data/%c/%s.%s",
	    stats_dir, parray[p1].login[0], parray[p1].login, STATS_GAMES);
	fp = fopen(fname, "r");	// old moves now looks in history to save mem
				// - DAV

	if (!fp) {
		pprintf(p, "There is no old game for %s.\n", parray[p1].name);

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

	while (!feof(fp))
		fgets(tmp, 1024, fp);

	sscanf(ptmp, "%d", &count);
	fclose(fp);
	pprintf(p, "Last game for %s was history game %d.\n", parray[p1].name,
	    count);

	if (mail)
		pcommand(p, "mailstored %s %d", parray[p1].name, count);
	else
		pcommand(p, "smoves %s %d", parray[p1].name, count);

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

PUBLIC int
com_oldmoves(int p, param_list param)
{
	return old_mail_moves(p, 0, param);
}

PUBLIC int
com_mailoldmoves(int p, param_list param)
{
	return old_mail_moves(p, 1, param);
}

PUBLIC void
ExamineScratch(int p, param_list param)
{
	char	*val;
	char	 board[100];
	char	 category[100];
	char	 parsebuf[100];
	int	 confused = 0;
	int	 g = game_new();

	unobserveAll(p);

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

	garray[g].bInitTime = garray[g].bIncrement = 0;
	garray[g].wInitTime = garray[g].wIncrement = 0;

	garray[g].startTime		= tenth_secs();
	garray[g].timeOfStart		= tenth_secs();

	garray[g].clockStopped		= 0;
	garray[g].lastDecTime		= garray[g].startTime;
	garray[g].lastMoveTime		= garray[g].startTime;
	garray[g].rated			= 0;
	garray[g].status		= GAME_EXAMINE;
	garray[g].totalHalfMoves	= 0;
	garray[g].type			= TYPE_UNTIMED;
	garray[g].wTime			= garray[g].bTime = 0;
	garray[g].white			= garray[g].black = p;
	parray[p].game			= g;
	parray[p].side			= WHITE;

	board[0]	= '\0';
	category[0]	= '\0';

	if (param[0].val.string != parray[p].name &&
	    param[1].type == TYPE_WORD) {
		mstrlcpy(category, param[0].val.string, sizeof category);
		mstrlcpy(board, param[1].val.string, sizeof board);
	} else if (param[1].type != TYPE_NULL) {
		val = param[1].val.string;

		while (!confused && sscanf(val, " %99s", parsebuf) == 1) {
			val = eatword(eatwhite(val));

			if (category[0] != '\0' && board[0] == '\0') {
				mstrlcpy(board, parsebuf, sizeof board);
			} else if (isdigit(*parsebuf)) {
				pprintf(p, "You can't specify time controls."
				    "\n");
				return;
			} else if (category[0] == '\0') {
				mstrlcpy(category, parsebuf, sizeof category);
			} else {
				confused = 1;
			}
		}

		if (confused) {
			pprintf(p, "Can't interpret %s in match command.\n",
			    parsebuf);
			return;
		}
	}

	if (category[0] && !board[0]) {
		pprintf(p, "You must specify a board and a category.\n");
		return;
	}

	pprintf(p, "Starting a game in examine (scratch) mode.\n");

	if (category[0]) {
		pprintf(p, "Loading from catagory: %s, board: %s.\n",
		    category,
		    board);
	}
	if (board_init(&garray[g].game_state, category, board)) {
		pprintf(p, "PROBLEM LOADING BOARD. Game Aborted.\n");
		fprintf(stderr, "FICS: PROBLEM LOADING BOARD. Game Aborted.\n");
		return;
	}

	garray[g].game_state.gameNum = g;
	mstrlcpy(garray[g].white_name, parray[p].name,
	    sizeof(garray[g].white_name));
	mstrlcpy(garray[g].black_name, parray[p].name,
	    sizeof(garray[g].black_name));
	garray[g].white_rating = garray[g].black_rating =
	    parray[p].s_stats.rating;
	send_boards(g);
	MakeFENpos(g, (char *)garray[g].FENstartPos,
	    ARRAY_SIZE(garray[g].FENstartPos));
}

PRIVATE int
ExamineStored(FILE *fp, int p, char *filename)
{
	char	 board[100];
	char	 category[100];
	game	*gg;
	int	 g;

	unobserveAll(p);

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

	g	= game_new();
	gg	= &garray[g];

	board[0]	= '\0';
	category[0]	= '\0';

	if (board_init(&gg->game_state, category, board)) {
		pprintf(p, "PROBLEM LOADING BOARD. Game Aborted.\n");
		fprintf(stderr, "FICS: PROBLEM LOADING BOARD %s %s. "
		    "Game Aborted.\n", category, board);
		return -1;
	}

	gg->status = GAME_EXAMINE;

	if (ReadGameAttrs(fp, filename, g) < 0) {
		pprintf(p, "Gamefile is corrupt; please notify an admin.\n");
		return -1;
	}

	gg->startTime		= tenth_secs();
	gg->lastDecTime		= gg->startTime;
	gg->lastMoveTime	= gg->startTime;

	gg->totalHalfMoves	= gg->numHalfMoves;
	gg->numHalfMoves	= 0;
	gg->revertHalfMove	= 0;

	gg->black		= p;
	gg->white		= p;
	gg->game_state.gameNum	= g;

	parray[p].game = g;
	parray[p].side = WHITE;

	send_boards(g);
	MakeFENpos(g, (char *)garray[g].FENstartPos,
	    ARRAY_SIZE(garray[g].FENstartPos));

	return g;
}

PRIVATE void
ExamineAdjourned(int p, int p1, int p2)
{
	FILE	*fp;
	char	*p1Login, *p2Login;
	char	 filename[1024] = { '\0' };
	int	 g;

	p1Login = parray[p1].login;
	p2Login = parray[p2].login;

	(void)snprintf(filename, sizeof filename, "%s/%c/%s-%s",
	    adj_dir, *p1Login, p1Login, p2Login);
	fp = fopen(filename, "r");

	if (!fp) {
		(void)snprintf(filename, sizeof filename, "%s/%c/%s-%s",
		    adj_dir, *p2Login, p1Login, p2Login);
		fp = fopen(filename, "r");

		if (!fp) {
			(void)snprintf(filename, sizeof filename,
			    "%s/%c/%s-%s",
			    adj_dir, *p2Login, p2Login, p1Login);
			fp = fopen(filename, "r");

			if (!fp) {
				(void)snprintf(filename, sizeof filename,
				    "%s/%c/%s-%s",
				    adj_dir, *p1Login, p2Login, p1Login);
				fp = fopen(filename, "r");

				if (!fp) {
					pprintf(p, "No stored game between "
					    "\"%s\" and \"%s\".\n",
					    parray[p1].name,
					    parray[p2].name);
					return;
				}
			}
		}
	}

	g = ExamineStored(fp, p, filename);
	fclose(fp);

	if (g >= 0) {
		if (garray[g].white_name[0] == '\0') {
			mstrlcpy(garray[g].white_name, p1Login,
			    sizeof(garray[g].white_name));
		}
		if (garray[g].black_name[0] == '\0') {
			mstrlcpy(garray[g].black_name, p2Login,
			    sizeof(garray[g].black_name));
		}
	}
}

PRIVATE char *
FindHistory(int p, int p1, int game)
{
	FILE		*fpHist;
	int		 index = 0;
	long int	 when = 0;
	static char	 fileName[MAX_FILENAME_SIZE];

	msnprintf(fileName, sizeof fileName, "%s/player_data/%c/%s.%s",
	    stats_dir, parray[p1].login[0], parray[p1].login, STATS_GAMES);

	if ((fpHist = fopen(fileName, "r")) == NULL) {
		pprintf(p, "No games in history for %s.\n", parray[p1].name);
		return NULL;
	}

	do {
		int ret;

		ret = fscanf(fpHist, "%d %*c %*d %*c %*d %*s %*s %*d %*d %*d "
		    "%*d %*s %*s %ld", &index, &when);
		if (ret != 2)
			warn("%s: %s: corrupt", __func__, &fileName[0]);
	} while (!feof(fpHist) && index != game);

	if (feof(fpHist)) {
		pprintf(p, "There is no history game %d for %s.\n", game,
		    parray[p1].name);
		fclose(fpHist);
		return NULL;
	}

	fclose(fpHist);

	msnprintf(fileName, sizeof fileName, "%s/%ld/%ld", hist_dir,
	    (when % 100), when);
	return (&fileName[0]);
}

PRIVATE char *
FindHistory2(int p, int p1, int game, char *End)
{
	FILE		*fpHist;
	int		 index = 0;
	long int	 when = 0;
	static char	 fileName[MAX_FILENAME_SIZE];

	msnprintf(fileName, sizeof fileName, "%s/player_data/%c/%s.%s",
	    stats_dir, parray[p1].login[0], parray[p1].login, STATS_GAMES);

	if ((fpHist = fopen(fileName, "r")) == NULL) {
		pprintf(p, "No games in history for %s.\n", parray[p1].name);
		return NULL;
	}

	do {
		int ret;

		ret = fscanf(fpHist, "%d %*c %*d %*c %*d %*s %*s %*d %*d %*d "
		    "%*d %*s %s %ld", &index, End, &when);
		if (ret != 3)
			warn("%s: %s: corrupt", __func__, &fileName[0]);
	} while (!feof(fpHist) && index != game);

	if (feof(fpHist)) {
		pprintf(p, "There is no history game %d for %s.\n", game,
		    parray[p1].name);
		fclose(fpHist);
		return NULL;
	}

	fclose(fpHist);

	msnprintf(fileName, sizeof fileName, "%s/%ld/%ld", hist_dir,
	    (when % 100), when);
	return (&fileName[0]);
}

PRIVATE void
ExamineHistory(int p, int p1, int game)
{
	FILE	*fpGame;
	char	*fileName;

	if ((fileName = FindHistory(p, p1, game)) != NULL) {
		if ((fpGame = fopen(fileName, "r")) == NULL) {
			pprintf(p, "History game %d not available for %s.\n",
			    game, parray[p1].name);
		} else {
			ExamineStored(fpGame, p, fileName);
			fclose(fpGame);
		}
	}
}

PRIVATE void
ExamineJournal(int p, int p1, char slot)
{
	FILE	*fpGame;
	char	*name_from = parray[p1].login;
	char	 fname[MAX_FILENAME_SIZE] = { '\0' };

	if ((parray[p1].jprivate) &&
	    (p != p1) &&
	    (parray[p].adminLevel < ADMIN_ADMIN)) {
		pprintf(p, "Sorry, this journal is private.\n");
		return;
	}

	if (((slot - 'a' - 1) > MAX_JOURNAL) &&
	    (parray[p1].adminLevel < ADMIN_ADMIN) &&
	    (!titled_player(p, parray[p1].login))) {
		pprintf(p, "%s's maximum journal entry is %c\n",
		    parray[p1].name,
		    toupper((char)(MAX_JOURNAL + 'A' - 1)));
		return;
	}

	msnprintf(fname, sizeof fname, "%s/%c/%s.%c", journal_dir, name_from[0],
	    name_from, slot);

	if ((fpGame = fopen(fname, "r")) == NULL) {
		pprintf(p, "Journal entry %c is not available for %s.\n",
		    toupper(slot),
		    parray[p1].name);
	} else {
		ExamineStored(fpGame, p, fname);
		fclose(fpGame);
	}
}

PUBLIC int
com_examine(int p, param_list param)
{
	char	*param2string;
	char	 fname[MAX_FILENAME_SIZE] = { '\0' };
	int	 p1, p2 = p, p1conn, p2conn = 1;

	if (parray[p].game >= 0 && garray[parray[p].game].status ==
	    GAME_EXAMINE) {
		pprintf(p, "You are already examining a game.\n");
	} else if (parray[p].game >= 0) {
		pprintf(p, "You are playing a game.\n");
	} else if (param[0].type == TYPE_NULL) {
		ExamineScratch(p, param);
	} else if (param[0].type == TYPE_WORD) {
		if (param[1].type == TYPE_WORD) {
			msnprintf(fname, sizeof fname, "%s/%s/%s", board_dir,
			    param[0].val.word, param[1].val.word);

			if (file_exists(fname)) {
				ExamineScratch(p, param);
				return COM_OK;
			}
		}

		if (!FindPlayer(p, param[0].val.word, &p1, &p1conn))
			return COM_OK;

		if (param[1].type == TYPE_INT)
			ExamineHistory(p, p1, param[1].val.integer);
		else {
			if (param[1].type == TYPE_WORD) {
				/*
				 * Lets check the journal
				 */
				param2string = param[1].val.word;

				if (strlen(param2string) == 1 &&
				    isalpha(param2string[0])) {
					ExamineJournal(p, p1, param2string[0]);

					if (!p1conn)
						player_remove(p1);
					return COM_OK;
				} else {
					if (!FindPlayer(p, param[1].val.word,
					    &p2, &p2conn)) {
						if (!p1conn)
							player_remove(p1);
						return COM_OK;
					}
				}
			}

			ExamineAdjourned(p, p1, p2);

			if (!p2conn)
				player_remove(p2);
		}

		if (!p1conn)
			player_remove(p1);
	}

	return COM_OK;
}

PUBLIC int
com_stored(int p, param_list param)
{
	DIR		*dirp;
	char		 dname[MAX_FILENAME_SIZE];
	int		 p1, connected;
#ifdef USE_DIRENT
	struct dirent	*dp;
#else
	struct direct	*dp;
#endif

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

	msnprintf(dname, sizeof dname, "%s/%c", adj_dir, parray[p1].login[0]);
	dirp = opendir(dname);

	if (!dirp) {
		pprintf(p, "Player %s has no games stored.\n", parray[p1].name);

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

	pprintf(p, "Stored games for %s:\n", parray[p1].name);

	for (dp = readdir(dirp); dp != NULL; dp = readdir(dirp)) {
		if (file_has_pname(dp->d_name, parray[p1].login)) {
			pprintf(p, "   %s vs. %s\n",
			    file_wplayer(dp->d_name),
			    file_bplayer(dp->d_name));
		}
	}

	closedir(dirp);
	pprintf(p, "\n");

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

PRIVATE void
stored_mail_moves(int p, int mail, param_list param)
{
	FILE	*fpGame;
	char	*fileName;
	char	*name_from;
	char	*param2string = NULL;
	char	 fileName2[MAX_FILENAME_SIZE];
	int	 g = -1;
	int	 wp, wconnected, bp, bconnected, gotit = 0;

	if (mail && !parray[p].registered) {
		pprintf(p, "Unregistered players cannot use mailstored.\n");
		return;
	}

	if (!FindPlayer(p, param[0].val.word, &wp, &wconnected))
		return;

	if (param[1].type == TYPE_INT) { /* look for a game from history */
		if ((fileName = FindHistory(p, wp, param[1].val.integer)) !=
		    NULL) {
			if ((fpGame = fopen(fileName, "r")) == NULL) {
				pprintf(p, "History game %d not available "
				    "for %s.\n",
				    param[1].val.integer,
				    parray[wp].name);
			} else {
				g = game_new();

				if (ReadGameAttrs(fpGame, fileName, g) < 0)
					pprintf(p, "Gamefile is corrupt; "
					    "please notify an admin.\n");
				else
					gotit = 1;

				fclose(fpGame);
			}
		}
	} else {
		/*
		 * Let's test for journal
		 */
		name_from = param[0].val.word;
		param2string = param[1].val.word;

		if (strlen(param2string) == 1 && isalpha(param2string[0])) {
			if (parray[wp].jprivate &&
			    parray[p].adminLevel < ADMIN_ADMIN &&
			    p != wp) {
				pprintf(p, "Sorry, the journal from which you "
				    "are trying to fetch is private.\n");
			} else {
				if ((param2string[0] - 'a' - 1) > MAX_JOURNAL &&
				    parray[wp].adminLevel < ADMIN_ADMIN &&
				    !titled_player(p, parray[wp].login)) {
					pprintf(p, "%s's maximum journal entry "
					    "is %c\n",
					    parray[wp].name,
					    toupper((char)(MAX_JOURNAL +
					    'A' - 1)));
				} else {
					msnprintf(fileName2, sizeof fileName2,
					    "%s/%c/%s.%c",
					    journal_dir,
					    name_from[0],
					    name_from,
					    param2string[0]);

					if ((fpGame = fopen(fileName2, "r")) ==
					    NULL) {
						pprintf(p, "Journal entry %c "
						    "is not available for %s.\n",
						    toupper(param2string[0]),
						    parray[wp].name);
					} else {
						g = game_new();

						/* XXX: was 'fileName' */
						if (ReadGameAttrs(fpGame,
						    fileName2, g) < 0)
							pprintf(p, "Journal "
							    "entry is corrupt; "
							    "please notify an "
							    "admin.\n");
						else
							gotit = 1;

						fclose(fpGame);
					}
				}
			}
		} else {
			/*
			 * look for a stored game between the players
			 */

			if (FindPlayer(p, param[1].val.word, &bp,
			    &bconnected)) {
				g = game_new();

				if (game_read(g, wp, bp) >= 0) {
					gotit = 1;
				} else if (game_read(g, bp, wp) >= 0) {
					gotit = 1;
				} else {
					pprintf(p, "There is no stored game "
					    "%s vs. %s\n",
					    parray[wp].name,
					    parray[bp].name);
				}

				if (!bconnected)
					player_remove(bp);
			}
		}
	}

	if (gotit) {
		if (strcasecmp(parray[p].name, garray[g].white_name) &&
		    strcasecmp(parray[p].name, garray[g].black_name) &&
		    garray[g].private &&
		    parray[p].adminLevel < ADMIN_ADMIN) {
			pprintf(p, "Sorry, that is a private game.\n");
		} else {
			if (mail == 1) { /* Do mailstored */
				char	subj[81];

				if (param[1].type == TYPE_INT) {
					msnprintf(subj, sizeof subj, "FICS "
					    "history game: %s %d",
					    parray[wp].name,
					    param[1].val.integer);
				} else {
					if (param2string == NULL) /* XXX */
						errx(1, "%s: param2string == "
						    "NULL", __func__);
					if (strlen(param2string) == 1 &&
					    isalpha(param2string[0])) {
						msnprintf(subj, sizeof subj,
						    "FICS journal "
						    "game %s vs %s",
						    garray[g].white_name,
						    garray[g].black_name);
					} else {
						msnprintf(subj, sizeof subj,
						    "FICS adjourned "
						    "game %s vs %s",
						    garray[g].white_name,
						    garray[g].black_name);
					}
				}
				if (mail_string_to_user(p, subj, movesToString
				    (g, parray[p].pgn)))
					pprintf(p, "Moves NOT mailed, perhaps "
					    "your address is incorrect.\n");
				else
					pprintf(p, "Moves mailed.\n");
			} else {
				pprintf(p, "%s\n", movesToString(g, 0));
			} /* Do smoves */
		}
	}

	if (!wconnected)
		player_remove(wp);
	if (g != -1)
		game_remove(g);
}

PUBLIC int
com_mailstored(int p, param_list param)
{
	stored_mail_moves(p, 1, param);
	return COM_OK;
}

PUBLIC int
com_smoves(int p, param_list param)
{
	stored_mail_moves(p, 0, param);
	return COM_OK;
}

PUBLIC int
com_sposition(int p, param_list param)
{
	int	g;
	int	wp, wconnected, bp, bconnected, confused = 0;

	if (!FindPlayer(p, param[0].val.word, &wp, &wconnected))
		return (COM_OK);
	if (!FindPlayer(p, param[1].val.word, &bp, &bconnected)) {
		if (!wconnected)
			player_remove(wp);
		return (COM_OK);
	}

	g = game_new();

	if (game_read(g, wp, bp) < 0) {		// if no game white-black,
		if (game_read(g, bp, wp) < 0) {	// look for black-white
			confused = 1;

			pprintf(p, "There is no stored game %s vs. %s\n",
			    parray[wp].name,
			    parray[bp].name);
		} else {
			int	tmp;

			tmp		= wp;
			wp		= bp;
			bp		= tmp;
			tmp		= wconnected;
			wconnected	= bconnected;
			bconnected	= tmp;
		}
	}

	if (!confused) {
		if ((wp != p) &&
		    (bp != p) &&
		    (garray[g].private) &&
		    (parray[p].adminLevel < ADMIN_ADMIN)) {
			pprintf(p, "Sorry, that is a private game.\n");
		} else {
			garray[g].white		= wp;
			garray[g].black		= bp;
			garray[g].startTime	= tenth_secs();
			garray[g].lastMoveTime	= garray[g].startTime;
			garray[g].lastDecTime	= garray[g].startTime;

			pprintf(p, "Position of stored game %s vs. %s\n",
			    parray[wp].name,
			    parray[bp].name);

			send_board_to(g, p);
		}
	}

	game_remove(g);

	if (!wconnected)
		player_remove(wp);
	if (!bconnected)
		player_remove(bp);
	return COM_OK;
}

PUBLIC int
com_forward(int p, param_list param)
{
	int		g, i;
	int		nHalfMoves = 1;
	int		p1;
	unsigned int	now;

	if (!(parray[p].game >= 0 && garray[parray[p].game].status ==
	    GAME_EXAMINE)) {
		pprintf(p, "You are not examining any games.\n");
		return COM_OK;
	}

	g = parray[p].game;

	if (!strcmp(garray[g].white_name, garray[g].black_name)) {
		pprintf(p, "You cannot go forward; no moves are stored.\n");
		return COM_OK;
	}

	if (param[0].type == TYPE_INT)
		nHalfMoves = param[0].val.integer;

	if (garray[g].numHalfMoves > garray[g].revertHalfMove) {
		pprintf(p, "No more moves.\n");
		return COM_OK;
	}

	if (garray[g].numHalfMoves < garray[g].totalHalfMoves) {
		for (p1 = 0; p1 < p_num; p1++) {
			if (parray[p1].status != PLAYER_PROMPT)
				continue;
			if (player_is_observe(p1, g) || parray[p1].game == g) {
				pprintf(p1, "%s goes forward %d move%s.\n",
				    parray[p].name,
				    nHalfMoves,
				    (nHalfMoves == 1 ? "" : "s"));
			}
		}
	}

	for (i = 0; i < nHalfMoves; i++) {
		if (garray[g].numHalfMoves < garray[g].totalHalfMoves) {
			execute_move(&garray[g].game_state,
			    &garray[g].moveList[garray[g].numHalfMoves], 1);

			if (garray[g].numHalfMoves + 1 >
			    garray[g].examMoveListSize) {
				// Allocate 20 moves at a time.
				garray[g].examMoveListSize += 20;

				if (!garray[g].examMoveList) {
					garray[g].examMoveList =
					    reallocarray(NULL,
					    sizeof(move_t),
					    garray[g].examMoveListSize);
					if (garray[g].examMoveList == NULL)
						err(1, "%s", __func__);
					malloc_count++;
				} else {
					garray[g].examMoveList =
					    reallocarray(garray[g].examMoveList,
					    sizeof(move_t),
					    garray[g].examMoveListSize);
					if (garray[g].examMoveList == NULL)
						err(1, "%s", __func__);
				}
			}

			garray[g].examMoveList[garray[g].numHalfMoves] =
			    garray[g].moveList[garray[g].numHalfMoves];
			garray[g].revertHalfMove++;
			garray[g].numHalfMoves++;
		} else {
			for (p1 = 0; p1 < p_num; p1++) {
				if (parray[p1].status != PLAYER_PROMPT)
					continue;
				if (player_is_observe(p1, g) ||
				    parray[p1].game == g)
					pprintf(p1, "End of game.\n");
			}

			break;
		}
	}

	// roll back time
	if (garray[g].game_state.onMove == WHITE) {
		garray[g].wTime += (garray[g].lastDecTime -
				    garray[g].lastMoveTime);
	} else {
		garray[g].bTime += (garray[g].lastDecTime -
				    garray[g].lastMoveTime);
	}

	now = tenth_secs();

	if (garray[g].numHalfMoves == 0)
		garray[g].timeOfStart = now;
	garray[g].lastMoveTime = now;
	garray[g].lastDecTime = now;

	send_boards(g);
	return COM_OK;
}

PUBLIC int
com_backward(int p, param_list param)
{
	int		g, i;
	int		nHalfMoves = 1;
	int		p1;
	unsigned int	now;

	if (!(parray[p].game >= 0 && garray[parray[p].game].status ==
	    GAME_EXAMINE)) {
		pprintf(p, "You are not examining any games.\n");
		return COM_OK;
	}

	g = parray[p].game;

	if (param[0].type == TYPE_INT)
		nHalfMoves = param[0].val.integer;

	if (garray[g].numHalfMoves != 0) {
		for (p1 = 0; p1 < p_num; p1++) {
			if (parray[p1].status != PLAYER_PROMPT)
				continue;
			if (player_is_observe(p1, g) || parray[p1].game == g) {
				pprintf(p1, "%s backs up %d move%s.\n",
				    parray[p].name,
				    nHalfMoves,
				    (nHalfMoves == 1 ? "" : "s"));
			}
		}
	}

	for (i = 0; i < nHalfMoves; i++) {
		if (backup_move(g, REL_EXAMINE) != MOVE_OK) {
			for (p1 = 0; p1 < p_num; p1++) {
				if (parray[p1].status != PLAYER_PROMPT)
					continue;
				if (player_is_observe(p1, g) ||
				    parray[p1].game == g)
					pprintf(p1, "Beginning of game.\n");
			}

			break;
		}
	}

	if (garray[g].numHalfMoves < garray[g].revertHalfMove)
		garray[g].revertHalfMove = garray[g].numHalfMoves;

	// roll back time
	if (garray[g].game_state.onMove == WHITE) {
		garray[g].wTime += (garray[g].lastDecTime -
				    garray[g].lastMoveTime);
	} else {
		garray[g].bTime += (garray[g].lastDecTime -
				    garray[g].lastMoveTime);
	}

	now = tenth_secs();

	if (garray[g].numHalfMoves == 0)
		garray[g].timeOfStart = now;
	garray[g].lastMoveTime = now;
	garray[g].lastDecTime = now;

	send_boards(g);
	return COM_OK;
}

PUBLIC int
com_revert(int p, param_list param)
{
	int		g, i;
	int		nHalfMoves = 1;
	int		p1;
	unsigned int	now;

	if (!(parray[p].game >= 0 && garray[parray[p].game].status ==
	    GAME_EXAMINE)) {
		pprintf(p, "You are not examining any games.\n");
		return COM_OK;
	}

	g = parray[p].game;
	nHalfMoves = garray[g].numHalfMoves - garray[g].revertHalfMove;

	if (nHalfMoves == 0) {
		pprintf(p, "Already at mainline.\n");
		return COM_OK;
	}

	if (nHalfMoves < 0) {	// eek - should NEVER happen!
		fprintf(stderr, "OUCH! in %s: nHalfMoves < 0\n", __func__);
		return COM_OK;
	}

	for (p1 = 0; p1 < p_num; p1++) {
		if (parray[p1].status != PLAYER_PROMPT)
			continue;
		if (player_is_observe(p1, g) || parray[p1].game == g) {
			pprintf(p1, "%s reverts to mainline.\n",
			    parray[p].name);
		}
	}

	for (i = 0; i < nHalfMoves; i++)
		backup_move(g, REL_EXAMINE);

	// roll back time
	if (garray[g].game_state.onMove == WHITE) {
		garray[g].wTime += (garray[g].lastDecTime -
				    garray[g].lastMoveTime);
	} else {
		garray[g].bTime += (garray[g].lastDecTime -
				    garray[g].lastMoveTime);
	}

	now = tenth_secs();

	if (garray[g].numHalfMoves == 0)
		garray[g].timeOfStart = now;
	garray[g].lastMoveTime = now;
	garray[g].lastDecTime = now;

	send_boards(g);
	return COM_OK;
}

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

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

	msnprintf(fname, sizeof fname, "%s/player_data/%c/%s.%s", stats_dir,
	    parray[p1].login[0], parray[p1].login, STATS_GAMES);
	pgames(p, p1, fname);

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

PUBLIC int
com_journal(int p, param_list param)
{
	char	fname[MAX_FILENAME_SIZE];
	int	p1, connected;

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

	if (!parray[p1].registered) {
		pprintf(p, "Only registered players may keep a journal.\n");

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

	if (parray[p1].jprivate &&
	    p != p1 &&
	    parray[p].adminLevel < ADMIN_ADMIN) {
		pprintf(p, "Sorry, this journal is private.\n");

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

	msnprintf(fname, sizeof fname, "%s/player_data/%c/%s.%s", stats_dir,
	    parray[p1].login[0], parray[p1].login, STATS_JOURNAL);
	pjournal(p, p1, fname);

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

PRIVATE void
jsave_journalentry(int p, char save_spot, int p1, char from_spot, char *to_file)
{
	FILE	*Game;
	char	*name_from = parray[p1].login;
	char	*name_to = parray[p].login;
	char	 BlackName[MAX_LOGIN_NAME + 1];
	char	 WhiteName[MAX_LOGIN_NAME + 1];
	char	 command[MAX_FILENAME_SIZE * 2 + 3];
	char	 eco[100];
	char	 ending[100];
	char	 fname[MAX_FILENAME_SIZE];
	char	 fname2[MAX_FILENAME_SIZE];
	char	 result[100];
	char	 type[100];
	int	 BlackRating;
	int	 WhiteRating;
	int	 i, t;

	msnprintf(fname, sizeof fname, "%s/%c/%s.%c", journal_dir, name_from[0],
	    name_from, from_spot);

	if ((Game = fopen(fname, "r")) == NULL) {
		pprintf(p, "Journal entry %c not available for %s.\n",
		    toupper(from_spot),
		    parray[p1].name);
		return;
	}

	fclose(Game);

	msnprintf(fname2, sizeof fname2, "%s/%c/%s.%c", journal_dir, name_to[0],
	    name_to, save_spot);
	unlink(fname2);

	msnprintf(command, sizeof command, "cp %s %s", fname, fname2);
	if (system(command)) { // XXX
		pprintf(p, "System command in jsave_journalentry failed!\n");
		pprintf(p, "Please report this to an admin.\n");
		fprintf(stderr, "FICS: System command failed in "
		    "jsave_journalentry\n");
		return;
	}

	msnprintf(fname, sizeof fname, "%s/player_data/%c/%s.%s", stats_dir,
	    name_to[0], name_to, STATS_JOURNAL);

	if (!journal_get_info(p, from_spot, WhiteName, &WhiteRating,
	    BlackName, &BlackRating, type, &t, &i, eco, ending, result, fname))
		return;

	addjournalitem(p, toupper(save_spot),
	    WhiteName, WhiteRating,
	    BlackName, BlackRating,
	    type, t, i, eco, ending, result, to_file);
	pprintf(p, "Journal entry %s %c saved in slot %c in journal.\n",
	    parray[p1].name, toupper(from_spot), toupper(save_spot));
}

PUBLIC void
jsave_history(int p, char save_spot, int p1, int from, char *to_file)
{
	FILE	*Game;
	char	*EndSymbol;
	char	*HistoryFname;
	char	*name_to = parray[p].login;
	char	 End[100];
	char	 command[MAX_FILENAME_SIZE * 2 + 3];
	char	 filename[MAX_FILENAME_SIZE + 1] = { '\0' }; // XXX
	char	 jfname[MAX_FILENAME_SIZE];
	char	 type[4];
	int	 g;

	if ((HistoryFname = FindHistory2(p, p1, from, End)) != NULL) {
		if ((Game = fopen(HistoryFname, "r")) == NULL) {
			pprintf(p, "History game %d not available for %s.\n",
			    from,
			    parray[p1].name);
		} else {
			msnprintf(jfname, sizeof jfname, "%s/%c/%s.%c",
			    journal_dir,
			    name_to[0],
			    name_to,
			    save_spot);
			unlink(jfname);

			msnprintf(command, sizeof command, "cp %s %s",
			    HistoryFname, jfname);
			if (system(command)) { // XXX: A little messy, but works
				pprintf(p, "System command in jsave_history "
				    "failed!\n");
				pprintf(p, "Please report this to an admin.\n");
				fprintf(stderr, "FICS: System command failed "
				    "in jsave_journalentry\n");
				fclose(Game);
				return;
			}

			g = game_new(); // Open a dummy game

			// XXX: is 'filename' right here?
			if (ReadGameAttrs(Game, filename, g) < 0) {
				pprintf(p, "Gamefile is corrupt. Please tell "
				    "an admin.\n");
				game_free(g);
				fclose(Game);
				return;
			}

			fclose(Game);

			if (garray[g].private) {
				type[0] = 'p';
			} else {
				type[0] = ' ';
			}

			if (garray[g].type == TYPE_BLITZ) {
				type[1] = 'b';
			} else if (garray[g].type == TYPE_WILD) {
				type[1] = 'w';
			} else if (garray[g].type == TYPE_STAND) {
				type[1] = 's';
			} else {
				if (garray[g].type == TYPE_NONSTANDARD)
					type[1] = 'n';
				else
					type[1] = 'u';
			}

			if (garray[g].rated) {
				type[2] = 'r';
			} else {
				type[2] = 'u';
			}

			type[3] = '\0';
			EndSymbol = EndSym(g);
			addjournalitem(p, toupper(save_spot),
			    garray[g].white_name, garray[g].white_rating,
			    garray[g].black_name, garray[g].black_rating,
			    type,
			    garray[g].wInitTime,
			    garray[g].wIncrement,
			    getECO(g), End, EndSymbol, to_file);
			game_free(g);
			pprintf(p, "Game %s %d saved in slot %c in journal.\n",
			    parray[p1].name, from, toupper(save_spot));
		}
	}
}

PUBLIC int
com_jsave(int p, param_list param)
{
	char	*from;
	char	*to = param[0].val.word;
	char	 fname[MAX_FILENAME_SIZE] = { '\0' };
	int	 p1, p1conn;

	if (!parray[p].registered) {
		pprintf(p, "Only registered players may keep a journal.\n");
		return COM_OK;
	}

	if (strlen(to) != 1 || !(isalpha(to[0]))) {
		pprintf(p, "Journal entries are referenced by single "
		    "letters.\n");
		return COM_OK;
	}

	if ((to[0] - 'a' - 1) > MAX_JOURNAL &&
	    parray[p].adminLevel < ADMIN_ADMIN &&
	    !titled_player(p, parray[p].login)) {
		pprintf(p, "Your maximum journal entry is %c\n",
		    toupper((char)(MAX_JOURNAL + 'A' - 1)));
		return COM_OK;
	}

	if (!FindPlayer(p, param[1].val.word, &p1, &p1conn))
		return COM_OK;

	if (param[2].type == TYPE_INT) {
		// grab from a history
		msnprintf(fname, sizeof fname, "%s/player_data/%c/%s.%s",
		    stats_dir, parray[p].login[0], parray[p].login,
		    STATS_JOURNAL);
		jsave_history(p, to[0], p1, param[2].val.integer, fname);
	} else {
		from = param[2].val.word;

		if (strlen(from) != 1 || !(isalpha(from[0]))) {
			pprintf(p, "Journal entries are referenced by single "
			    "letters.\n");
			if (!p1conn)
				player_remove(p1);
			return COM_OK;
		}

		if (parray[p1].jprivate &&
		    parray[p].adminLevel < ADMIN_ADMIN &&
		    p != p1) {
			pprintf(p, "Sorry, the journal from which you are "
			    "trying to fetch is private.\n");
			if (!p1conn)
				player_remove(p1);
			return COM_OK;
		}

		if ((to[0] - 'a' - 1) > MAX_JOURNAL &&
		    parray[p1].adminLevel < ADMIN_ADMIN &&
		    !titled_player(p, parray[p1].login)) {
			pprintf(p, "%s's maximum journal entry is %c\n",
			    parray[p1].name,
			    toupper((char)(MAX_JOURNAL + 'A' - 1)));

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

		if ((p == p1) && (to[0] == from[0])) {
			pprintf(p, "Source and destination entries are the "
			    "same.\n");
			return COM_OK;
		}

		// grab from a journal
		msnprintf(fname, sizeof fname, "%s/player_data/%c/%s.%s",
		    stats_dir, parray[p].login[0], parray[p].login,
		    STATS_JOURNAL);
		jsave_journalentry(p, to[0], p1, from[0], fname);
	}

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