/* gamedb.c
 *
 */

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

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

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

/* Revision history:
   name		email		yy/mm/dd	Change
   Richard Nash                 93/10/22	Created
   Markus Uhlin                 23/12/16	Fixed compiler warnings
   Markus Uhlin                 23/12/17	Reformatted functions
   Markus Uhlin                 23/12/23	Fixed clang warnings
   Markus Uhlin                 24/05/04	Refactored and reformatted all
						functions plus more...
   Markus Uhlin                 24/07/17	Fixed out of bounds array access
						in game_str() which resulted in
						a crash.
   Markus Uhlin                 24/07/18	Return value checking
   Markus Uhlin                 24/08/03	See previous change
   Markus Uhlin                 24/11/23	Added width specifications to
						multiple fscanf() calls.
   Markus Uhlin                 24/11/23	Fixed bugs in movesToString()
*/

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

#include <err.h>

#include "command.h"
#include "config.h"
#include "eco.h"
#include "ficsmain.h"
#include "gamedb.h"
#include "gameproc.h"
#include "maxxes-utils.h"
#include "network.h"
#include "playerdb.h"
#include "rmalloc.h"
#include "utils.h"

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

/*
 * This should be enough to hold any game up to at least 250 moves. If
 * we overwrite this, the server will crash.
 */
#define GAME_STRING_LEN 16000

PUBLIC game	*garray = NULL;
PUBLIC int	 g_num = 0;

PUBLIC const char *bstr[7] = {
	[TYPE_UNTIMED]     = "untimed",
	[TYPE_BLITZ]       = "blitz",
	[TYPE_STAND]       = "standard",
	[TYPE_NONSTANDARD] = "non-standard",
	[TYPE_WILD]        = "wild",
	[TYPE_LIGHT]       = "lightning",
	[TYPE_BUGHOUSE]    = "bughouse"
};
PUBLIC const char *rstr[2] = {
	[TYPE_UNRATED] = "unrated",
	[TYPE_RATED] = "rated"
};

PRIVATE char gameString[GAME_STRING_LEN];

/*
 * This method is awful! How about allocation as we need it and
 * freeing afterwards!
 */
PRIVATE int
get_empty_slot(void)
{
	for (int i = 0; i < g_num; i++) {
		if (garray[i].status == GAME_EMPTY)
			return i;
	}

	g_num++;

	if (!garray) {
		garray = reallocarray(NULL, sizeof(game), g_num);

		if (garray == NULL)
			err(1, "%s: reallocarray", __func__);
		else
			malloc_count++;
	} else {
		garray = reallocarray(garray, sizeof(game), g_num);

		if (garray == NULL)
			err(1, "%s: reallocarray", __func__);
	}	// Yeah great, bet this causes lag! - DAV

	garray[g_num - 1].status = GAME_EMPTY;
	return g_num - 1;
}

PUBLIC int
game_new(void)
{
	int	new = get_empty_slot();

	game_zero(new);

	return new;
}

PUBLIC int
game_zero(int g)
{
	garray[g].white = -1;
	garray[g].black = -1;

	garray[g].link		= -1;
	garray[g].passes	= 0;
	garray[g].private	= 0;
	garray[g].rated		= 0;
	garray[g].result	= END_NOTENDED;
	garray[g].status	= GAME_NEW;
	garray[g].type		= TYPE_UNTIMED;

	board_init(&garray[g].game_state, NULL, NULL);

	garray[g].bIncrement		= 0;
	garray[g].bInitTime		= 300; // 5 minutes
	garray[g].black_name[0]		= '\0';
	garray[g].black_rating		= 0;

	garray[g].wIncrement		= 0;
	garray[g].wInitTime		= 300; // 5 minutes
	garray[g].white_name[0]		= '\0';
	garray[g].white_rating		= 0;

	garray[g].examMoveList		= NULL;
	garray[g].examMoveListSize	= 0;

#ifdef TIMESEAL
	garray[g].flag_check_time	= 0L;
	garray[g].flag_pending		= FLAG_NONE;
#endif

	garray[g].game_state.gameNum	= g;
	garray[g].moveList		= NULL;
	garray[g].moveListSize		= 0;
	garray[g].numHalfMoves		= 0;
	garray[g].revertHalfMove	= 0;

	return 0;
}

PUBLIC int
game_free(int g)
{
	if (garray[g].moveListSize)
		rfree(garray[g].moveList);
	if (garray[g].examMoveListSize)
		rfree(garray[g].examMoveList);

	garray[g].moveListSize = 0;
	garray[g].examMoveListSize = 0;

	return 0;
}

PUBLIC int
game_clear(int g)
{
	game_free(g);
	game_zero(g);
	return 0;
}

PUBLIC int
game_remove(int g)
{
	// Should remove game from players observation list
	game_clear(g);
	garray[g].status = GAME_EMPTY;
	return 0;
}

// old moves not stored now - uses smoves
PUBLIC int
game_finish(int g)
{
	player_game_ended(g);	// Alert playerdb that game ended
	game_remove(g);
	return 0;
}

PUBLIC void
MakeFENpos(int g, char *FEN, size_t size)
{
	mstrlcpy(FEN, boardToFEN(g), size);
}

PUBLIC char *
game_time_str(int wt, int winc, int bt, int binc)
{
	static char	tstr[50] = { '\0' };

	if ((!wt) && (!winc)) { // Untimed
		(void) strlcpy(tstr, "", sizeof tstr);
		return tstr;
	}

	if ((wt == bt) && (winc == binc)) {
		msnprintf(tstr, sizeof tstr, " %d %d", wt, winc);
	} else {
		msnprintf(tstr, sizeof tstr, " %d %d : %d %d",
		    wt, winc,
		    bt, binc);
	}

	return tstr;
}

PUBLIC char *
game_str(int rated, int wt, int winc, int bt, int binc, char *cat, char *board)
{
	static char	tstr[200] = { '\0' };

	if (rated != TYPE_UNRATED &&
	    rated != TYPE_RATED)
		rated = TYPE_UNRATED;

	if (cat && cat[0] && board && board[0] && (strcmp(cat, "standard") ||
	    strcmp(board, "standard"))) {
		msnprintf(tstr, sizeof(tstr), "%s %s%s Loaded from %s/%s",
		    rstr[rated],
		    bstr[game_isblitz(wt / 60, winc, bt / 60, binc, cat, board)],
		    game_time_str(wt / 60, winc, bt / 60, binc),
		    cat,
		    board);
	} else {
		msnprintf(tstr, sizeof(tstr), "%s %s%s",
		    rstr[rated],
		    bstr[game_isblitz(wt / 60, winc, bt / 60, binc, cat, board)],
		    game_time_str(wt / 60, winc, bt / 60, binc));
	}

	return tstr;
}

PUBLIC int
game_isblitz(int wt, int winc, int bt, int binc, char *cat, char *board)
{
	int total;

	if (cat && cat[0] && board && board[0] && (!strcmp(cat, "wild")))
		return TYPE_WILD;
	if (cat && cat[0] && board && board[0] && (strcmp(cat, "standard") ||
	    strcmp(board, "standard")))
		return TYPE_NONSTANDARD;

	if (((wt == 0) && (winc == 0)) || ((bt == 0) && (binc == 0))) {
		/*
		 * nonsense if one is timed and one is not
		 */
		return TYPE_UNTIMED;
	}

	if ((wt != bt) || (winc != binc))
		return TYPE_NONSTANDARD;

	total = wt * 60 + winc * 40;

	if (total < 180)	// 3 minutes
		return TYPE_LIGHT;
	if (total >= 900)	// 15 minutes
		return TYPE_STAND;
	else
		return TYPE_BLITZ;
}

PUBLIC void
send_board_to(int g, int p)
{
	char	*b;
	int	 relation;
	int	 side;

	/*
	 * Since we know 'g' and 'p', figure out our relationship to
	 * this game...
	 */

	side = WHITE;

	if (garray[g].status == GAME_EXAMINE) {
		if (parray[p].game == g) {
			relation = 2;
		} else {
			relation = -2;
		}
	} else {
		if (parray[p].game == g) {
			side = parray[p].side;
			relation = (side == garray[g].game_state.onMove ? 1 :
			    -1);
		} else {
			relation = 0;
		}
	}

	if (parray[p].flip) {	// flip board?
		if (side == WHITE)
			side = BLACK;
		else
			side = WHITE;
	}

	game_update_time(g);
	b = board_to_string(garray[g].white_name, garray[g].black_name,
	    garray[g].wTime,
	    garray[g].bTime,
	    &garray[g].game_state,
	    (garray[g].status == GAME_EXAMINE)
	    ? garray[g].examMoveList
	    : garray[g].moveList,
	    parray[p].style,
	    side,
	    relation,
	    p);

#ifdef TIMESEAL
	if (con[parray[p].socket].timeseal) {
		if (parray[p].bell) {
			pprintf_noformat(p, "\007\n[G]\n%s", b);
		} else {
			pprintf_noformat(p, "\n[G]\n%s", b);
		}
	} else {
		if (parray[p].bell) {
			pprintf_noformat(p, "\007\n%s", b);
		} else {
			pprintf_noformat(p, "\n%s", b);
		}
	}
#else
	if (parray[p].bell) {
		pprintf_noformat(p, "\007\n%s", b);
	} else {
		pprintf_noformat(p, "\n%s", b);
	}
#endif

	if (p != commanding_player)
		pprintf(p, "%s", parray[p].prompt);
}

PUBLIC void
send_boards(int g)
{
	simul_info_t *simInfo = &parray[garray[g].white].simul_info;

	if (simInfo->numBoards == 0 || simInfo->boards[simInfo->onBoard] == g) {
		for (int p = 0; p < p_num; p++) {
			if (parray[p].status == PLAYER_EMPTY)
				continue;
			if (player_is_observe(p, g) || (parray[p].game == g))
				send_board_to(g, p);
		}
	}
}

PUBLIC void
game_update_time(int g)
{
	unsigned int now, timesince;

	if (garray[g].clockStopped)
		return;
	if (garray[g].type == TYPE_UNTIMED)
		return;

	now = tenth_secs();
	timesince = now - garray[g].lastDecTime;

	if (garray[g].game_state.onMove == WHITE) {
		garray[g].wTime -= timesince;
	} else {
		garray[g].bTime -= timesince;
	}

	garray[g].lastDecTime = now;
}

PUBLIC void
game_update_times(void)
{
	for (int g = 0; g < g_num; g++) {
		if (garray[g].status != GAME_ACTIVE)
			continue;
		if (garray[g].clockStopped)
			continue;
		game_update_time(g);
	}
}

PUBLIC char *
EndString(int g, int personal)
{
	/*
	 * personal 0 == White checkmated
	 * personal 1 == loon checkmated
	 */
	char		*blackguy, *whiteguy;
	static char	 blackstr[] = "Black";
	static char	 whitestr[] = "White";
	static char	 endstr[200] = { '\0' };

	blackguy = (personal ? garray[g].black_name : blackstr);
	whiteguy = (personal ? garray[g].white_name : whitestr);

	switch (garray[g].result) {
	case END_CHECKMATE:
		msnprintf(endstr, sizeof endstr, "%s checkmated",
		    garray[g].winner == WHITE ? blackguy : whiteguy);
		break;
	case END_RESIGN:
		msnprintf(endstr, sizeof endstr, "%s resigned",
		    garray[g].winner == WHITE ? blackguy : whiteguy);
		break;
	case END_FLAG:
		msnprintf(endstr, sizeof endstr, "%s ran out of time",
		    garray[g].winner == WHITE ? blackguy : whiteguy);
		break;
	case END_AGREEDDRAW:
		msnprintf(endstr, sizeof endstr, "Game drawn by mutual "
		    "agreement");
		break;
	case END_BOTHFLAG:
		msnprintf(endstr, sizeof endstr, "Game drawn because both "
		    "players ran out of time");
		break;
	case END_REPETITION:
		msnprintf(endstr, sizeof endstr, "Game drawn by repetition");
		break;
	case END_50MOVERULE:
		msnprintf(endstr, sizeof endstr, "Draw by the 50 move rule");
		break;
	case END_ADJOURN:
		msnprintf(endstr, sizeof endstr, "Game adjourned by mutual "
		    "agreement");
		break;
	case END_LOSTCONNECTION:
		msnprintf(endstr, sizeof endstr, "%s lost connection, "
		    "game adjourned",
		    garray[g].winner == WHITE ? whiteguy : blackguy);
		break;
	case END_ABORT:
		msnprintf(endstr, sizeof endstr, "Game aborted by mutual "
		    "agreement");
		break;
	case END_STALEMATE:
		msnprintf(endstr, sizeof endstr, "Stalemate.");
		break;
	case END_NOTENDED:
		msnprintf(endstr, sizeof endstr, "Still in progress");
		break;
	case END_COURTESY:
		msnprintf(endstr, sizeof endstr, "Game courtesyaborted by %s",
		    garray[g].winner == WHITE ? whiteguy : blackguy);
		break;
	case END_COURTESYADJOURN:
		msnprintf(endstr, sizeof endstr, "Game courtesyadjourned by %s",
		    garray[g].winner == WHITE ? whiteguy : blackguy);
		break;
	case END_NOMATERIAL:
		msnprintf(endstr, sizeof endstr, "Game drawn because neither "
		    "player has mating material");
		break;
	case END_FLAGNOMATERIAL:
		msnprintf(endstr, sizeof endstr, "%s ran out of time and %s "
		    "has no material to mate",
		    garray[g].winner == WHITE ? blackguy : whiteguy,
		    garray[g].winner == WHITE ? whiteguy : blackguy);
		break;
	case END_ADJDRAW:
		msnprintf(endstr, sizeof endstr, "Game drawn by adjudication");
		break;
	case END_ADJWIN:
		msnprintf(endstr, sizeof endstr, "%s wins by adjudication",
		    garray[g].winner == WHITE ? whiteguy : blackguy);
		break;
	case END_ADJABORT:
		msnprintf(endstr, sizeof endstr, "Game aborted by "
		    "adjudication");
		break;
	default:
		msnprintf(endstr, sizeof endstr, "???????");
		break;
	}

	return endstr;
}

PUBLIC char *
EndSym(int g)
{
	static char *symbols[] = {
		"1-0",
		"0-1",
		"1/2-1/2",
		"*"
	};

	switch (garray[g].result) {
	case END_CHECKMATE:
	case END_RESIGN:
	case END_FLAG:
	case END_ADJWIN:
		return ((garray[g].winner == WHITE) ? symbols[0] : symbols[1]);
		break;
	case END_AGREEDDRAW:
	case END_BOTHFLAG:
	case END_REPETITION:
	case END_50MOVERULE:
	case END_STALEMATE:
	case END_NOMATERIAL:
	case END_FLAGNOMATERIAL:
	case END_ADJDRAW:
		return (symbols[2]);
		break;
	}

	return (symbols[3]);
}

PUBLIC char *
movesToString(int g, int pgn)
{
	char		*serv_loc = SERVER_LOCATION;
	char		*serv_name = SERVER_NAME;
	char		 tmp[160] = { '\0' };
	int		 i, col;
	int		 wr, br;
	struct tm	*tm_ptr = NULL;
	time_t		 curTime;

	wr = garray[g].white_rating;
	br = garray[g].black_rating;

	curTime = untenths(garray[g].timeOfStart);

	if (pgn) {
		msnprintf(gameString, sizeof gameString,
		    "\n[Event \"%s %s %s game\"]\n"
		    "[Site \"%s, %s\"]\n",
		    serv_name,
		    rstr[garray[g].rated],
		    bstr[garray[g].type],
		    serv_name,
		    serv_loc);

		if ((tm_ptr = localtime(&curTime)) != NULL) {
			strftime(tmp, sizeof(tmp),
			    "[Date \"%Y.%m.%d\"]\n"
			    "[Time \"%H:%M:%S\"]\n",
			    tm_ptr);
			mstrlcat(gameString, tmp, sizeof gameString);
		} else
			warn("%s: localtime", __func__);

		msnprintf(tmp, sizeof tmp,
		    "[Round \"-\"]\n"
		    "[White \"%s\"]\n"
		    "[Black \"%s\"]\n"
		    "[WhiteElo \"%d\"]\n"
		    "[BlackElo \"%d\"]\n",
		    garray[g].white_name,
		    garray[g].black_name,
		    wr, br);
		mstrlcat(gameString, tmp, sizeof gameString);

		msnprintf(tmp, sizeof tmp,
		    "[TimeControl \"%d+%d\"]\n"
		    "[Mode \"ICS\"]\n"
		    "[Result \"%s\"]\n\n",
		    garray[g].wInitTime / 10,
		    garray[g].wIncrement / 10,
		    EndSym(g));
		mstrlcat(gameString, tmp, sizeof gameString);

		col = 0;

		for (i = 0; i < garray[g].numHalfMoves; i++) {
			if (!(i % 2)) {
				if ((col += snprintf(tmp, sizeof tmp, "%d. ",
				    i / 2 + 1)) > 70) {
					mstrlcat(gameString, "\n",
					    sizeof gameString);
					col = 0;
				}

				mstrlcat(gameString, tmp, sizeof gameString);
			}

			if ((col += snprintf(tmp, sizeof tmp, "%s ",
			    (garray[g].status == GAME_EXAMINE)
			    ? garray[g].examMoveList[i].algString
			    : garray[g].moveList[i].algString)) > 70) {
				mstrlcat(gameString, "\n", sizeof gameString);
				col = 0;
			}

			mstrlcat(gameString, tmp, sizeof gameString);
		}

		mstrlcat(gameString, "\n", sizeof gameString);
	} else {
		/*
		 * !pgn
		 */

		msnprintf(gameString, sizeof gameString, "\n%s ",
		    garray[g].white_name);

		if (wr > 0) {
			msnprintf(tmp, sizeof tmp, "(%d) ", wr);
		} else {
			msnprintf(tmp, sizeof tmp, "(UNR) ");
		}

		mstrlcat(gameString, tmp, sizeof gameString);
		msnprintf(tmp, sizeof tmp, "vs. %s ", garray[g].black_name);
		mstrlcat(gameString, tmp, sizeof gameString);

		if (br > 0) {
			msnprintf(tmp, sizeof tmp, "(%d) ", br);
		} else {
			msnprintf(tmp, sizeof tmp, "(UNR) ");
		}

		mstrlcat(gameString, tmp, sizeof gameString);
		mstrlcat(gameString, "--- ", sizeof gameString);

		if ((tm_ptr = localtime(&curTime)) != NULL) {
			strftime(tmp, sizeof tmp, "%Y.%m.%d %H:%M:%S", tm_ptr);
			mstrlcat(gameString, tmp, sizeof gameString);
		} else
			warn("%s: localtime", __func__);

		if (garray[g].rated) {
			mstrlcat(gameString, "\nRated ", sizeof gameString);
		} else {
			mstrlcat(gameString, "\nUnrated ", sizeof gameString);
		}

		if (garray[g].type == TYPE_BLITZ) {
			mstrlcat(gameString, "Blitz ", sizeof(gameString));
		} else if (garray[g].type == TYPE_LIGHT) {
			mstrlcat(gameString, "Lighting ", sizeof(gameString));
		} else if (garray[g].type == TYPE_BUGHOUSE) {
			mstrlcat(gameString, "Bughouse ", sizeof(gameString));
		} else if (garray[g].type == TYPE_STAND) {
			mstrlcat(gameString, "Standard ", sizeof(gameString));
		} else if (garray[g].type == TYPE_WILD) {
			mstrlcat(gameString, "Wild ", sizeof(gameString));
		} else if (garray[g].type == TYPE_NONSTANDARD) {
			mstrlcat(gameString, "Non-standard ",
			    sizeof(gameString));
		} else {
			mstrlcat(gameString, "Untimed ", sizeof(gameString));
		}

		mstrlcat(gameString, "match, initial time: ",
		    sizeof gameString);

		if ((garray[g].bInitTime != garray[g].wInitTime) ||
		    (garray[g].wIncrement != garray[g].bIncrement)) {
			/*
			 * different starting times
			 */

			msnprintf(tmp, sizeof tmp, "%d minutes, increment: %d "
			    "seconds AND %d minutes, increment: %d seconds."
			    "\n\n",
			    garray[g].wInitTime / 600,
			    garray[g].wIncrement / 10,
			    garray[g].bInitTime / 600,
			    garray[g].bIncrement / 10);
		} else {
			msnprintf(tmp, sizeof tmp, "%d minutes, increment: "
			    "%d seconds.\n\n",
			    garray[g].wInitTime / 600,
			    garray[g].wIncrement / 10);
		}

		mstrlcat(gameString, tmp, sizeof gameString);
		msnprintf(tmp, sizeof tmp, "Move  %-19s%-19s\n",
		    garray[g].white_name,
		    garray[g].black_name);
		mstrlcat(gameString, tmp, sizeof gameString);
		mstrlcat(gameString, "----  ----------------   ----------------"
		    "\n", sizeof gameString);

		for (i = 0; i < garray[g].numHalfMoves; i += 2) {
			if (i + 1 < garray[g].numHalfMoves) {
				msnprintf(tmp, sizeof tmp, "%3d.  %-16s   ",
				    i / 2 + 1,
				    (garray[g].status == GAME_EXAMINE)
				    ? move_and_time(&garray[g].examMoveList[i])
				    : move_and_time(&garray[g].moveList[i]));

				mstrlcat(gameString, tmp, sizeof gameString);

				msnprintf(tmp, sizeof tmp, "%-16s\n",
				    (garray[g].status == GAME_EXAMINE)
				    ? move_and_time(&garray[g].examMoveList[i + 1])
				    : move_and_time(&garray[g].moveList[i + 1]));
			} else {
				msnprintf(tmp, sizeof tmp, "%3d.  %-16s\n",
				    i / 2 + 1,
				    (garray[g].status == GAME_EXAMINE)
				    ? move_and_time(&garray[g].examMoveList[i])
				    : move_and_time(&garray[g].moveList[i]));
			}

			mstrlcat(gameString, tmp, sizeof gameString);

			if (strlen(gameString) > GAME_STRING_LEN - 100)
				return gameString;	// Bug out if getting
							// close to filling this
							// string
		}

		mstrlcat(gameString, "      ", sizeof gameString);
	}

	msnprintf(tmp, sizeof tmp, "{%s} %s\n", EndString(g, 0), EndSym(g));
	mstrlcat(gameString, tmp, sizeof gameString);

	return gameString;
}

PUBLIC void
game_disconnect(int g, int p)
{
	game_ended(g, (garray[g].white == p) ? WHITE : BLACK,
	    END_LOSTCONNECTION);
}

PUBLIC int
CharToPiece(char c)
{
	switch (c) {
	case 'P':
		return W_PAWN;
	case 'p':
		return B_PAWN;
	case 'N':
		return W_KNIGHT;
	case 'n':
		return B_KNIGHT;
	case 'B':
		return W_BISHOP;
	case 'b':
		return B_BISHOP;
	case 'R':
		return W_ROOK;
	case 'r':
		return B_ROOK;
	case 'Q':
		return W_QUEEN;
	case 'q':
		return B_QUEEN;
	case 'K':
		return W_KING;
	case 'k':
		return B_KING;
	default:
		return NOPIECE;
	}
}

PUBLIC int
PieceToChar(int piece)
{
	switch (piece) {
	case W_PAWN:
		return 'P';
	case B_PAWN:
		return 'p';
	case W_KNIGHT:
		return 'N';
	case B_KNIGHT:
		return 'n';
	case W_BISHOP:
		return 'B';
	case B_BISHOP:
		return 'b';
	case W_ROOK:
		return 'R';
	case B_ROOK:
		return 'r';
	case W_QUEEN:
		return 'Q';
	case B_QUEEN:
		return 'q';
	case W_KING:
		return 'K';
	case B_KING:
		return 'k';
	default:
		return ' ';
	}
}

/* One line has everything on it */
PRIVATE int
WriteMoves(FILE *fp, move_t *m)
{
	int		 i;
	int		 piece, castle;
	int		 useFile = 0, useRank = 0, check = 0;
	unsigned long	 MoveInfo = (m->color == BLACK);

	castle = (m->moveString[0] == 'o');

	if (castle)
		piece = KING;
	else
		piece = piecetype(CharToPiece(m->moveString[0]));

#define ORIGINAL_CODE 0
#if ORIGINAL_CODE
	MoveInfo	= (MoveInfo <<= 3) | piece;
	MoveInfo	= (MoveInfo <<= 3) | m->fromFile;
	MoveInfo	= (MoveInfo <<= 3) | m->fromRank;
	MoveInfo	= (MoveInfo <<= 3) | m->toFile;
	MoveInfo	= (MoveInfo <<= 3) | m->toRank;
	MoveInfo	= (MoveInfo <<= 3) | (m->pieceCaptured & 7);
	MoveInfo	= (MoveInfo <<= 3) | (m->piecePromotionTo & 7);
	MoveInfo	= (MoveInfo <<= 1) | (m->enPassant != 0);
#else
	MoveInfo <<= 3;
	MoveInfo |= piece;

	MoveInfo <<= 3;
	MoveInfo |= m->fromFile;

	MoveInfo <<= 3;
	MoveInfo |= m->fromRank;

	MoveInfo <<= 3;
	MoveInfo |= m->toFile;

	MoveInfo <<= 3;
	MoveInfo |= m->toRank;

	MoveInfo <<= 3;
	MoveInfo |= (m->pieceCaptured & 7);

	MoveInfo <<= 3;
	MoveInfo |= (m->piecePromotionTo & 7);

	MoveInfo <<= 1;
	MoveInfo |= (m->enPassant != 0);
#endif

	/* Are we using from-file or from-rank in 'algString'? */

	i = strlen(m->algString) - 1;

	if (m->algString[i] == '+') {
		check = 1;
		i--;
	}

	if (piece != PAWN && !castle) {
		i -= 2;

		if (i < 0)
			return -1;
		if (m->algString[i] == 'x')
			i--;
		if (i < 0)
			return -1;
		if (isdigit(m->algString[i])) {
			useRank = 2;
			i--;
		}
		if (i < 0)
			return -1;

		useFile = (islower(m->algString[i]) ? 4 : 0);
	}

	MoveInfo = ((MoveInfo << 3) | useFile | useRank | check);

	fprintf(fp, "%lx %x %x\n", MoveInfo, m->tookTime, m->atTime);
	return 0;
}

PRIVATE int
ReadMove(FILE *fp, move_t *m)
{
	char	line[MAX_GLINE_SIZE] = { '\0' };

	if (fgets(line, sizeof line, fp) == NULL)
		return -1;

	_Static_assert(ARRAY_SIZE(m->moveString) > 7, "'moveString' too small");
	_Static_assert(ARRAY_SIZE(m->algString) > 7,  "'algString' too small");

	if (sscanf(line, "%d %d %d %d %d %d %d %d %d \"%7[^\"]\" \"%7[^\"]\" "
	    "%u %u\n",
	    &m->color,
	    &m->fromFile, &m->fromRank,
	    &m->toFile, &m->toRank,
	    &m->pieceCaptured,
	    &m->piecePromotionTo,
	    &m->enPassant,
	    &m->doublePawn,
	    m->moveString,
	    m->algString,
	    &m->atTime,
	    &m->tookTime) != 13)
		return -1;

	return 0;
}

PRIVATE void
WriteGameState(FILE *fp, game_state_t *gs)
{
	int	i, j;

	for (i = 0; i < 8; i++) {
		for (j = 0; j < 8; j++)
			fprintf(fp, "%c", PieceToChar(gs->board[i][j]));
	}

	fprintf(fp, "%d %d %d %d %d %d",
		gs->wkmoved, gs->wqrmoved, gs->wkrmoved,
		gs->bkmoved, gs->bqrmoved, gs->bkrmoved);

	for (i = 0; i < 8; i++) {
		fprintf(fp, " %d %d", gs->ep_possible[0][i],
		    gs->ep_possible[1][i]);
	}

	fprintf(fp, " %d %d %d\n", gs->lastIrreversable, gs->onMove,
	    gs->moveNum);
}

PRIVATE int
ReadGameState(FILE *fp, game_state_t *gs, int version)
{
	char	pieceChar;
	int	i, j;
	int	wkmoved, wqrmoved, wkrmoved, bkmoved, bqrmoved, bkrmoved;

	if (version == 0) {
		for (i = 0; i < 8; i++) {
			for (j = 0; j < 8; j++) {
				if (fscanf(fp, "%d ", &gs->board[i][j]) != 1)
					return -1;
			}
		}
	} else {
		getc(fp);	/* Skip past a newline. */

		for (i = 0; i < 8; i++) {
			for (j = 0; j < 8; j++) {
				pieceChar = getc(fp);
				gs->board[i][j] = CharToPiece(pieceChar);
			}
		}
	}

	if (fscanf(fp, "%d %d %d %d %d %d",
	    &wkmoved, &wqrmoved, &wkrmoved,
	    &bkmoved, &bqrmoved, &bkrmoved) != 6)
		return -1;

	gs->wkmoved	= wkmoved;
	gs->wqrmoved	= wqrmoved;
	gs->wkrmoved	= wkrmoved;

	gs->bkmoved	= bkmoved;
	gs->bqrmoved	= bqrmoved;
	gs->bkrmoved	= bkrmoved;

	for (i = 0; i < 8; i++) {
		if (fscanf(fp, " %d %d", &gs->ep_possible[0][i],
		    &gs->ep_possible[1][i]) != 2)
			return -1;
	}

	if (fscanf(fp, " %d %d %d\n", &gs->lastIrreversable, &gs->onMove,
	    &gs->moveNum) != 3)
		return -1;
	return 0;
}

PUBLIC int
got_attr_value(int g, char *attr, char *value, FILE *fp, char *file)
{
	if (!strcmp(attr, "w_init:")) {
		garray[g].wInitTime = atoi(value);
	} else if (!strcmp(attr, "w_inc:")) {
		garray[g].wIncrement = atoi(value);
	} else if (!strcmp(attr, "b_init:")) {
		garray[g].bInitTime = atoi(value);
	} else if (!strcmp(attr, "b_inc:")) {
		garray[g].bIncrement = atoi(value);
	} else if (!strcmp(attr, "white_name:")) {
		mstrlcpy(garray[g].white_name, value,
		    sizeof(garray[g].white_name));
	} else if (!strcmp(attr, "black_name:")) {
		mstrlcpy(garray[g].black_name, value,
		    sizeof(garray[g].black_name));
	} else if (!strcmp(attr, "white_rating:")) {
		garray[g].white_rating = atoi(value);
	} else if (!strcmp(attr, "black_rating:")) {
		garray[g].black_rating = atoi(value);
	} else if (!strcmp(attr, "result:")) {
		garray[g].result = atoi(value);
	} else if (!strcmp(attr, "timestart:")) {
		garray[g].timeOfStart = atoi(value);
	} else if (!strcmp(attr, "w_time:")) {
		garray[g].wTime = atoi(value);
	} else if (!strcmp(attr, "b_time:")) {
		garray[g].bTime = atoi(value);
	} else if (!strcmp(attr, "clockstopped:")) {
		garray[g].clockStopped = atoi(value);
	} else if (!strcmp(attr, "rated:")) {
		garray[g].rated = atoi(value);
	} else if (!strcmp(attr, "private:")) {
		garray[g].private = atoi(value);
	} else if (!strcmp(attr, "type:")) {
		garray[g].type = atoi(value);
	} else if (!strcmp(attr, "halfmoves:")) {
		garray[g].numHalfMoves = atoi(value);

		if (garray[g].numHalfMoves == 0)
			return 0;

		garray[g].moveListSize = garray[g].numHalfMoves;
		garray[g].moveList = reallocarray(NULL, sizeof(move_t),
		    garray[g].moveListSize);

		if (garray[g].moveList == NULL)
			err(1, "%s: reallocarray", __func__);
		else
			malloc_count++;

		for (int i = 0; i < garray[g].numHalfMoves; i++) {
			if (ReadMove(fp, &garray[g].moveList[i])) {
				fprintf(stderr, "FICS: Trouble reading moves "
				    "from %s.\n", file);
				return -1;
			}
		}
	} else if (!strcmp(attr, "gamestate:")) {	// Value meaningless
		if (garray[g].status != GAME_EXAMINE &&
		    ReadGameState(fp, &garray[g].game_state, 0)) {
			fprintf(stderr, "FICS: Trouble reading game state "
			    "from %s.\n", file);
			return -1;
		}
	} else {
		fprintf(stderr, "FICS: Error bad attribute >%s< from file %s\n",
		    attr, file);
	}

	return 0;
}

PRIVATE int
ReadOneV1Move(FILE *fp, move_t *m)
{
	char		 PieceChar;
	int		 i;
	int		 useFile, useRank, check, piece;
	unsigned long	 MoveInfo;

	if (fscanf(fp, "%lx %x %x", &MoveInfo, &m->tookTime, &m->atTime) != 3)
		return -1;

	check = MoveInfo & 1;
	useRank = MoveInfo & 2;
	useFile = MoveInfo & 4;

	MoveInfo >>= 3;
	m->enPassant = (MoveInfo & 1);		// May have to negate later.

	MoveInfo >>= 1;
	m->piecePromotionTo = (MoveInfo & 7);	// May have to change color.

	MoveInfo >>= 3;
	m->pieceCaptured = (MoveInfo & 7);	// May have to change color.

	MoveInfo >>= 3;
	m->toRank = (MoveInfo & 7);

	MoveInfo >>= 3;
	m->toFile = (MoveInfo & 7);

	MoveInfo >>= 3;
	m->fromRank = (MoveInfo & 7);

	MoveInfo >>= 3;
	m->fromFile = (MoveInfo & 7);

	MoveInfo >>= 3;
	piece = (MoveInfo & 7);

	m->color = ((MoveInfo & 8) ? BLACK : WHITE);

	if (m->pieceCaptured != NOPIECE) {
		if (m->color == BLACK)
			m->pieceCaptured |= WHITE;
		else
			m->pieceCaptured |= BLACK;
	}

	if (piece == PAWN) {
		PieceChar = 'P';

		if ((m->toRank == 3 && m->fromRank == 1) ||
		    (m->toRank == 4 && m->fromRank == 6))
			m->doublePawn = m->toFile;
		else
			m->doublePawn = -1;

		if (m->pieceCaptured) {
			msnprintf(m->algString, sizeof m->algString, "%cx%c%d",
			    ('a' + m->fromFile),
			    ('a' + m->toFile),
			    (m->toRank + 1));
		} else {
			msnprintf(m->algString, sizeof m->algString, "%c%d",
			    ('a' + m->toFile),
			    (m->toRank + 1));
		}

		if (m->piecePromotionTo != 0) {
			if (m->piecePromotionTo == KNIGHT) {
				mstrlcat(m->algString, "=N",
				    sizeof(m->algString));
			} else if (m->piecePromotionTo == BISHOP) {
				mstrlcat(m->algString, "=B",
				    sizeof(m->algString));
			} else if (m->piecePromotionTo == ROOK) {
				mstrlcat(m->algString, "=R",
				    sizeof(m->algString));
			} else if (m->piecePromotionTo == QUEEN) {
				mstrlcat(m->algString, "=Q",
				    sizeof(m->algString));
			}

			m->piecePromotionTo |= m->color;
		}

		if (m->enPassant)
			m->enPassant = (m->toFile - m->fromFile);
	} else {
		m->doublePawn	= -1;
		PieceChar	= PieceToChar(piecetype(piece) | WHITE);

		if (PieceChar == 'K' && m->fromFile == 4 && m->toFile == 6) {
			mstrlcpy(m->algString, "O-O", sizeof(m->algString));
			mstrlcpy(m->moveString, "o-o", sizeof(m->moveString));
		} else if (PieceChar == 'K' &&
		    m->fromFile == 4 &&
		    m->toFile == 2) {
			mstrlcpy(m->algString, "O-O-O", sizeof(m->algString));
			mstrlcpy(m->moveString, "o-o-o", sizeof(m->moveString));
		} else {
			i = 0;
			m->algString[i++] = PieceChar;

			if (useFile)
				m->algString[i++] = 'a' + m->fromFile;
			if (useRank)
				m->algString[i++] = '1' + m->fromRank;
			if (m->pieceCaptured != 0)
				m->algString[i++] = 'x';

			m->algString[i++]	= ('a' + m->toFile);
			m->algString[i++]	= ('1' + m->toRank);
			m->algString[i]		= '\0';
		}
	}

	if (m->algString[0] != 'O') {
		int ret, too_long;

		ret = snprintf(m->moveString, sizeof m->moveString,
		    "%c/%c%d-%c%d",
		    PieceChar,
		    ('a' + m->fromFile),
		    (m->fromRank + 1),
		    ('a' + m->toFile),
		    (m->toRank + 1));

		too_long = (ret < 0 || (size_t)ret >= sizeof m->moveString);

		if (too_long) {
			fprintf(stderr, "FICS: %s: warning: "
			    "snprintf truncated\n", __func__);
		}
	}
	if (check)
		mstrlcat(m->algString, "+", sizeof m->algString);
	return 0;
}

PRIVATE int
ReadV1Moves(game *g, FILE *fp)
{
	g->moveListSize = g->numHalfMoves;
	g->moveList = reallocarray(NULL, sizeof(move_t), g->moveListSize);

	if (g->moveList == NULL)
		err(1, "%s: reallocarray", __func__);
	else
		malloc_count++;

	for (int i = 0; i < g->numHalfMoves; i++) {
		if (ReadOneV1Move(fp, &g->moveList[i]) == -1) {
			warnx("%s: failed to read move %d/%d", __func__, i,
			    g->numHalfMoves);
			return -1;
		}
	}

	return 0;
}

PRIVATE int
ReadV1GameFmt(game *g, FILE *fp, const char *file, int version)
{
	int		ret[3];
	long int	lval;

	_Static_assert(17 < ARRAY_SIZE(g->white_name), "Unexpected array size");
	_Static_assert(17 < ARRAY_SIZE(g->black_name), "Unexpected array size");

	ret[0] = fscanf(fp, "%17s %17s", g->white_name, g->black_name);
	ret[1] = fscanf(fp, "%d %d", &g->white_rating, &g->black_rating);
	ret[2] = fscanf(fp, "%d %d %d %d",
	    &g->wInitTime,
	    &g->wIncrement,
	    &g->bInitTime,
	    &g->bIncrement);
	if (ret[0] != 2 ||
	    ret[1] != 2 ||
	    ret[2] != 4) {
		warnx("%s: fscanf error: %s", __func__, file);
		return -1;
	}

	if (version < 3 && !g->bInitTime)
		g->bInitTime = g->wInitTime;

	if (fscanf(fp, "%ld", &lval) != 1) {
		warnx("%s: %s: failed to get time of start", __func__, file);
		return -1;
	} else
		g->timeOfStart = lval;

	if (fscanf(fp, "%d %d", &g->wTime, &g->bTime) != 2) {
		warnx("%s: %s: failed to get 'wTime' and 'bTime'", __func__,
		    file);
		return -1;
	}

	if (version > 1) {
		if (fscanf(fp, "%d %d", &g->result, &g->winner) != 2) {
			warnx("%s: %s: failed to get 'result' nor 'winner'",
			    __func__, file);
			return -1;
		}
	} else {
		if (fscanf(fp, "%d", &g->result) != 1) {
			warnx("%s: %s: failed to get 'result'",
			    __func__, file);
			return -1;
		}
	}

	ret[0] = fscanf(fp, "%d %d %d %d", &g->private, &g->type, &g->rated,
	    &g->clockStopped);
	ret[1] = fscanf(fp, "%d", &g->numHalfMoves);
	if (ret[0] != 4 || ret[1] != 1) {
		warnx("%s: fscanf error: %s", __func__, file);
		return -1;
	}

	if (ReadV1Moves(g, fp) != 0) {
		warnx("%s: failed to read moves: %s", __func__, file);
		return -1;
	}

	if (g->status != GAME_EXAMINE &&
	    ReadGameState(fp, &g->game_state, version)) {
		fprintf(stderr, "FICS: Trouble reading game state from %s.\n",
		    file);
		return -1;
	}

	return 0;
}

PUBLIC int
ReadGameAttrs(FILE *fp, char *fname, int g)
{
	char	*attr, *value;
	char	 line[MAX_GLINE_SIZE] = { '\0' };
	int	 len;
	int	 version = 0;

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

	if (line[0] == 'v') {
		if (sscanf(line, "%*c %d", &version) != 1)
			warn("%s: failed to get version", __func__);
	}

	if (version > 0) {
		if (ReadV1GameFmt(&garray[g], fp, fname, version) == -1)
			return -1;
	} else {
		do {
			if ((len = strlen(line)) <= 1) {
				fgets(line, sizeof line, fp);
				continue;
			}

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

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

			value = eatword(attr);

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

			*value = '\0';
			value++;
			value = eatwhite(value);

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

			stolower(attr);

			if (got_attr_value(g, attr, value, fp, fname))
				return -1;

			fgets(line, sizeof line, fp);
		} while (!feof(fp));
	}

	if (!(garray[g].bInitTime))
		garray[g].bInitTime = garray[g].wInitTime;
	return 0;
}

PUBLIC int
game_read(int g, int wp, int bp)
{
	FILE	*fp;
	char	 fname[MAX_FILENAME_SIZE] = { '\0' };

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

#if 0
	garray[g].old_white = -1;
	garray[g].old_black = -1;
#endif

	garray[g].moveListSize = 0;
	garray[g].game_state.gameNum = g;

	mstrlcpy(garray[g].white_name, parray[wp].name,
	    sizeof(garray[g].white_name));
	mstrlcpy(garray[g].black_name, parray[bp].name,
	    sizeof(garray[g].black_name));

	if (garray[g].type == TYPE_BLITZ) {
		garray[g].white_rating = parray[wp].b_stats.rating;
		garray[g].black_rating = parray[bp].b_stats.rating;
	} else if (garray[g].type == TYPE_WILD) {
		garray[g].white_rating = parray[wp].w_stats.rating;
		garray[g].black_rating = parray[bp].w_stats.rating;
	} else if (garray[g].type == TYPE_LIGHT) {
		garray[g].white_rating = parray[wp].l_stats.rating;
		garray[g].black_rating = parray[bp].l_stats.rating;
	} else if (garray[g].type == TYPE_BUGHOUSE) {
		garray[g].white_rating = parray[wp].bug_stats.rating;
		garray[g].black_rating = parray[bp].bug_stats.rating;
	} else {
		garray[g].white_rating = parray[wp].s_stats.rating;
		garray[g].black_rating = parray[bp].s_stats.rating;
	}

	msnprintf(fname, sizeof fname, "%s/%c/%s-%s", adj_dir,
	    parray[wp].login[0], parray[wp].login, parray[bp].login);
	fp = fopen(fname, "r");

	if (!fp) {
		return -1;
	}

	if (ReadGameAttrs(fp, fname, g) < 0) {
		fclose(fp);
		return -1;
	}

	fclose(fp);

	if (garray[g].result == END_ADJOURN || garray[g].result ==
	    END_COURTESYADJOURN)
		garray[g].result = END_NOTENDED;

	garray[g].status = GAME_ACTIVE;
	garray[g].startTime = tenth_secs();
	garray[g].lastMoveTime = garray[g].startTime;
	garray[g].lastDecTime = garray[g].startTime;

	// Need to do notification and pending cleanup
	return 0;
}

PUBLIC int
game_delete(int wp, int bp)
{
	char	fname[MAX_FILENAME_SIZE];
	char	lname[MAX_FILENAME_SIZE];

	msnprintf(fname, sizeof fname, "%s/%c/%s-%s", adj_dir,
	    parray[wp].login[0], parray[wp].login, parray[bp].login);
	msnprintf(lname, sizeof lname, "%s/%c/%s-%s", adj_dir,
	    parray[bp].login[0], parray[wp].login, parray[bp].login);

	unlink(fname);
	unlink(lname);
	return 0;
}

PRIVATE void
WriteGameFile(FILE *fp, int g)
{
	game		*gg = &garray[g];
	long int	 lval;
	player		*bp = &parray[gg->black];
	player		*wp = &parray[gg->white];

	fprintf(fp, "v %d\n", GAMEFILE_VERSION);
	fprintf(fp, "%s %s\n", wp->name, bp->name);
	fprintf(fp, "%d %d\n", gg->white_rating, gg->black_rating);
	fprintf(fp, "%d %d %d %d\n", gg->wInitTime, gg->wIncrement,
	    gg->bInitTime, gg->bIncrement);

	lval = gg->timeOfStart;
	fprintf(fp, "%ld\n", lval);

#ifdef TIMESEAL
	fprintf(fp, "%d %d\n",
	    (con[wp->socket].timeseal ? (gg->wRealTime / 100) : gg->wTime),
	    (con[bp->socket].timeseal ? (gg->bRealTime / 100) : gg->bTime));
#endif

	fprintf(fp, "%d %d\n", gg->result, gg->winner);
	fprintf(fp, "%d %d %d %d\n", gg->private, gg->type, gg->rated,
	    gg->clockStopped);
	fprintf(fp, "%d\n", gg->numHalfMoves);

	for (int i = 0; i < garray[g].numHalfMoves; i++)
		WriteMoves(fp, &garray[g].moveList[i]);

	WriteGameState(fp, &garray[g].game_state);
}

PUBLIC int
game_save(int g)
{
	FILE	*fp;
	char	 fname[MAX_FILENAME_SIZE];
	char	 lname[MAX_FILENAME_SIZE];
	game	*gg = &garray[g];
	player	*wp, *bp;

	wp	= &parray[gg->white];
	bp	= &parray[gg->black];

	msnprintf(fname, sizeof fname, "%s/%c/%s-%s", adj_dir, wp->login[0],
	    wp->login, bp->login);
	msnprintf(lname, sizeof lname, "%s/%c/%s-%s", adj_dir, bp->login[0],
	    wp->login, bp->login);

	fp = fopen(fname, "w");

	if (!fp) {
		fprintf(stderr, "FICS: Problem opening file %s for write\n",
		    fname);
		return -1;
	}

	WriteGameFile(fp, g);

#if 0
	fprintf(fp, "W_Init: %d\n", garray[g].wInitTime);
	fprintf(fp, "W_Inc: %d\n", garray[g].wIncrement);
	fprintf(fp, "B_Init: %d\n", garray[g].bInitTime);
	fprintf(fp, "B_Inc: %d\n", garray[g].bIncrement);
	fprintf(fp, "white_name: %s\n", wp->name);
	fprintf(fp, "black_name: %s\n", bp->name);
	fprintf(fp, "white_rating: %d\n", garray[g].white_rating);
	fprintf(fp, "black_rating: %d\n", garray[g].black_rating);
	fprintf(fp, "result: %d\n", garray[g].result);
	fprintf(fp, "TimeStart: %d\n", (int) garray[g].timeOfStart);
	fprintf(fp, "W_Time: %d\n", garray[g].wTime);
	fprintf(fp, "B_Time: %d\n", garray[g].bTime);
	fprintf(fp, "ClockStopped: %d\n", garray[g].clockStopped);
	fprintf(fp, "Rated: %d\n", garray[g].rated);
	fprintf(fp, "Private: %d\n", garray[g].private);
	fprintf(fp, "Type: %d\n", garray[g].type);
	fprintf(fp, "HalfMoves: %d\n", garray[g].numHalfMoves);

	for (int i = 0; i < garray[g].numHalfMoves; i++)
		WriteMoves(fp, &garray[g].moveList[i]);

	fprintf(fp, "GameState: IsNext\n");
	WriteGameState(fp, &garray[g].game_state);
#endif

	fclose(fp);

	/*
	 * Create link for easier stored game finding
	 */
	if (bp->login[0] != wp->login[0])
		link(fname, lname);
	return 0;
}

PRIVATE long int
OldestHistGame(char *login)
{
	FILE		*fp;
	char		 pFile[MAX_FILENAME_SIZE] = { '\0' };
	long int	 when;

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

	fp = fopen(pFile, "r");

	if (fp == NULL) {
		msnprintf(pFile, sizeof pFile, "%s/player_data/%c/.rem.%s.%s",
		    stats_dir, login[0], login, STATS_GAMES);
		fp = fopen(pFile, "r");
	}

	if (fp != NULL) {
		if (fscanf(fp, "%*d %*c %*d %*c %*d %*s %*s %*d %*d %*d %*d "
		    "%*s %*s %ld", &when) != 1) {
			warnx("%s: %s: failed to read 'when'", __func__,
			    &pFile[0]);
			fclose(fp);
			return 0L;
		}
		fclose(fp);
		return when;
	} else
		return 0L;
}

PRIVATE void
RemoveHistGame(char *file, int maxlines)
{
	FILE		*fp;
	char		 GameFile[MAX_FILENAME_SIZE] = { '\0' };
	char		 Opponent[MAX_LOGIN_NAME + 1] = { '\0' };
	char		 line[MAX_LINE_SIZE] = { '\0' };
	int		 count = 0;
	long int	 When, oppWhen;

	_Static_assert(20 < ARRAY_SIZE(Opponent), "Not within bounds");
	When = oppWhen = 0;

	if ((fp = fopen(file, "r")) == NULL) {
		return;
	} else if (fgets(line, ARRAY_SIZE(line), fp) == NULL) {
		warnx("%s: fgets error (file: %s)", __func__, file);
		fclose(fp);
		return;
	} else if (sscanf(line, "%*d %*c %*d %*c %*d %20s %*s %*d %*d %*d "
	    "%*d %*s %*s %ld", Opponent, &When) != 2) {
		warnx("%s: unexpected initial line (file: %s)", __func__, file);
		fclose(fp);
		return;
	}

	count++;

	while (!feof(fp)) {
		fgets(line, ARRAY_SIZE(line), fp);

		if (!feof(fp))
			count++;
	}

	fclose(fp);
	stolower(Opponent);

	if (count > maxlines) {
		truncate_file(file, maxlines);
		oppWhen = OldestHistGame(Opponent);

		if (oppWhen > When || oppWhen <= 0L) {
			msnprintf(GameFile, sizeof GameFile, "%s/%ld/%ld",
			    hist_dir, (When % 100), When);
			unlink(GameFile);
		}
	}
}

PUBLIC void
RemHist(char *who)
{
	FILE		*fp;
	char		 Opp[MAX_LOGIN_NAME] = { '\0' };
	char		 fName[MAX_FILENAME_SIZE] = { '\0' };
	long int	 When, oppWhen;

	msnprintf(fName, sizeof fName, "%s/player_data/%c/%s.%s", stats_dir,
	    who[0], who, STATS_GAMES);

	if ((fp = fopen(fName, "r")) != NULL) {
		long int iter_no = 0;

		while (!feof(fp)) {
			// XXX: end the format string with a newline?
			const int ret = fscanf(fp, "%*d %*c %*d %*c %*d %19s "
			    "%*s %*d %*d %*d %*d %*s %*s %ld", Opp, &When);
			if (ret != 2) {
				warnx("%s: fscanf() error (%s:%ld)", __func__,
				    fName, iter_no);
				iter_no++;
				continue;
			}

			stolower(Opp);
			oppWhen = OldestHistGame(Opp);

			if (oppWhen > When || oppWhen <= 0L) {
				char histfile[MAX_FILENAME_SIZE] = { '\0' };

				msnprintf(histfile, sizeof histfile,
				    "%s/%ld/%ld", hist_dir, (When % 100), When);
				if (unlink(histfile) != 0) {
					warn("%s: unlink(%s)", __func__,
					    histfile);
				}
			}

			iter_no++;
		}

		fclose(fp);
	}
}

PRIVATE void
write_g_out(int g, char *file, int maxlines, int isDraw, char *EndSymbol,
    char *name, time_t *now)
{
	FILE	*fp;
	char	*goteco;
	char	 cResult;
	char	 tmp[2048] = { '\0' };
	char	*ptmp = tmp;
	char	 type[4];
	int	 count = -1;
	int	 wp, bp;
	int	 wr, br;

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

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

	if (garray[g].type == TYPE_BLITZ) {
		wr = parray[wp].b_stats.rating;
		br = parray[bp].b_stats.rating;

		type[1] = 'b';
	} else if (garray[g].type == TYPE_WILD) {
		wr = parray[wp].w_stats.rating;
		br = parray[bp].w_stats.rating;

		type[1] = 'w';
	} else if (garray[g].type == TYPE_STAND) {
		wr = parray[wp].s_stats.rating;
		br = parray[bp].s_stats.rating;

		type[1] = 's';
	} else if (garray[g].type == TYPE_LIGHT) {
		wr = parray[wp].l_stats.rating;
		br = parray[bp].l_stats.rating;

		type[1] = 'l';
	} else if (garray[g].type == TYPE_BUGHOUSE) {
		wr = parray[wp].bug_stats.rating;
		br = parray[bp].bug_stats.rating;

		type[1] = 'd';
	} else {
		wr = 0;
		br = 0;

		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';

	fp = fopen(file, "r");

	if (fp) {
		while (!feof(fp))
			fgets(tmp, 1024, fp);
		if (sscanf(ptmp, "%d", &count) != 1)
			warnx("%s: failed to read 'count'", __func__);
		fclose(fp);
	}

	count = (count + 1) % 100;

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

	/*
	 * Counter
	 * Result
	 * MyRating
	 * MyColor
	 * OppRating
	 * OppName [pbr 2 12 2 12]
	 * ECO
	 * End
	 * Date
	 */
	if (name == parray[wp].name) {
		if (isDraw)
			cResult = '=';
		else if (garray[g].winner == WHITE)
			cResult = '+';
		else
			cResult = '-';

		fprintf(fp, "%d %c %d W %d %s %s %d %d %d %d %s %s %ld\n",
		    count, cResult, wr, br, parray[bp].name, type,
		    garray[g].wInitTime, garray[g].wIncrement,
		    garray[g].bInitTime, garray[g].bIncrement,
		    goteco,
		    EndSymbol,
		    (long int) *now);
	} else {
		if (isDraw)
			cResult = '=';
		else if (garray[g].winner == BLACK)
			cResult = '+';
		else
			cResult = '-';

		fprintf(fp, "%d %c %d B %d %s %s %d %d %d %d %s %s %ld\n",
		    count, cResult, br, wr, parray[wp].name, type,
		    garray[g].wInitTime, garray[g].wIncrement,
		    garray[g].bInitTime, garray[g].bIncrement,
		    goteco,
		    EndSymbol,
		    (long int) *now);
	}

	fclose(fp);
	RemoveHistGame(file, maxlines);
}

/*
 * Find from_spot in journal list - return 0 if corrupted
 */
PUBLIC int
journal_get_info(int p, char from_spot, char *WhiteName, int *WhiteRating,
    char *BlackName, int *BlackRating, char *type, int *t, int *i, char *eco,
    char *ending, char *result, char *fname)
{
	FILE	*fp;
	char	 count;

	if ((fp = fopen(fname, "r")) == NULL) {
		fprintf(stderr, "Corrupt journal file! %s\n", fname);
		pprintf(p, "The journal file is corrupt! See an admin.\n");
		return 0;
	}

	while (!feof(fp)) {
		if (fscanf(fp, "%c %s %d %s %d %s %d %d %s %s %s\n",
		    &count,
		    WhiteName, &(*WhiteRating),
		    BlackName, &(*BlackRating),
		    type,
		    &(*t), &(*i),
		    eco,
		    ending,
		    result) != 11) {
			fprintf(stderr, "FICS: Error in journal info format. "
			    "%s\n", fname);
			pprintf(p, "The journal file is corrupt! Error in "
			    "internal format.\n");
			fclose(fp);
			return 0;
		}

		if (tolower(count) == from_spot) {
			fclose(fp);
			return 1;
		}
	}

	fclose(fp);
	return 0;
}

PUBLIC void
addjournalitem(int p, char count2, char *WhiteName2, int WhiteRating2,
    char *BlackName2, int BlackRating2, char *type2, int t2, int i2,
    char *eco2, char *ending2, char *result2, char *fname)
{
	FILE	*fp;
	FILE	*fp2;
	char	 BlackName[MAX_LOGIN_NAME + 1] = { '\0' };
	char	 WhiteName[MAX_LOGIN_NAME + 1] = { '\0' };
	char	 count;
	char	 eco[100] = { '\0' };
	char	 ending[100] = { '\0' };
	char	 fname2[MAX_FILENAME_SIZE] = { '\0' };
	char	 result[100] = { '\0' };
	char	 type[100] = { '\0' };
	int	 WhiteRating, BlackRating;
	int	 have_output = 0;
	int	 t, i;

	mstrlcpy(fname2, fname, sizeof fname2);
	mstrlcat(fname2, ".w", sizeof fname2);

	if ((fp2 = fopen(fname2, "w")) == NULL) {
		fprintf(stderr, "FICS: Problem opening file %s for write\n",
		    fname);
		pprintf(p, "Couldn't update journal! Report this to an admin."
		    "\n");
		return;
	}

	if ((fp = fopen(fname, "r")) == NULL) { // Empty?
		fprintf(fp2, "%c %s %d %s %d %s %d %d %s %s %s\n",
		    count2,
		    WhiteName2, WhiteRating2,
		    BlackName2, BlackRating2,
		    type2,
		    t2, i2,
		    eco2,
		    ending2,
		    result2);
		fclose(fp2);
		rename(fname2, fname);
		return;
	} else {
		_Static_assert(ARRAY_SIZE(WhiteName) > 19,
		    "'WhiteName' too small");
		_Static_assert(ARRAY_SIZE(BlackName) > 19,
		    "'BlackName' too small");

		_Static_assert(ARRAY_SIZE(type) > 99,   "'type' too small");
		_Static_assert(ARRAY_SIZE(eco) > 99,    "'eco' too small");
		_Static_assert(ARRAY_SIZE(ending) > 99, "'ending' too small");
		_Static_assert(ARRAY_SIZE(result) > 99, "'result' too small");

		while (!feof(fp)) {
			if (fscanf(fp, "%c %19s %d %19s %d %99s %d %d %99s "
			    "%99s %99s\n",
			    &count,
			    WhiteName, &WhiteRating,
			    BlackName, &BlackRating,
			    type,
			    &t, &i,
			    eco,
			    ending,
			    result) != 11) {
				fprintf(stderr, "FICS: Error in journal info "
				    "format - aborting. %s\n", fname);
				fclose(fp);
				fclose(fp2);
				return;
			}

			if ((count >= count2) && (!have_output)) {
				fprintf(fp2, "%c %s %d %s %d %s %d %d %s %s %s\n",
				    count2,
				    WhiteName2, WhiteRating2,
				    BlackName2, BlackRating2,
				    type2,
				    t2, i2,
				    eco2,
				    ending2,
				    result2);
				have_output = 1;
			}

			if (count != count2) {
				fprintf(fp2, "%c %s %d %s %d %s %d %d %s %s %s"
				    "\n",
				    count,
				    WhiteName, WhiteRating,
				    BlackName, BlackRating,
				    type,
				    t, i,
				    eco,
				    ending,
				    result);
			}
		}

		if (!have_output) {	// Haven't written yet
			fprintf(fp2, "%c %s %d %s %d %s %d %d %s %s %s\n",
			    count2,
			    WhiteName2, WhiteRating2,
			    BlackName2, BlackRating2,
			    type2,
			    t2, i2,
			    eco2,
			    ending2,
			    result2);
		}
	}

	fclose(fp);
	fclose(fp2);

	rename(fname2, fname);
}

PUBLIC int
pjournal(int p, int p1, char *fname)
{
	FILE	*fp;
	char	 BlackName[MAX_LOGIN_NAME + 1] = { '\0' };
	char	 WhiteName[MAX_LOGIN_NAME + 1] = { '\0' };
	char	 count;
	char	 eco[100] = { '\0' };
	char	 ending[100] = { '\0' };
	char	 result[100] = { '\0' };
	char	 type[100] = { '\0' };
	int	 WhiteRating, BlackRating;
	int	 t, i;

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

	pprintf(p, "Journal for %s:\n", parray[p1].name);
	pprintf(p, "   White         Rating  Black         Rating  "
	    "Type         ECO End Result\n");

	_Static_assert(ARRAY_SIZE(WhiteName) > 19, "'WhiteName' too small");
	_Static_assert(ARRAY_SIZE(BlackName) > 19, "'BlackName' too small");

	_Static_assert(ARRAY_SIZE(type) > 99,   "'type' too small");
	_Static_assert(ARRAY_SIZE(eco) > 99,    "'eco' too small");
	_Static_assert(ARRAY_SIZE(ending) > 99, "'ending' too small");
	_Static_assert(ARRAY_SIZE(result) > 99, "'result' too small");

	while (!feof(fp)) {
		if (fscanf(fp, "%c %19s %d %19s %d %99s %d %d %99s %99s %99s\n",
		    &count,
		    WhiteName, &WhiteRating,
		    BlackName, &BlackRating,
		    type,
		    &t, &i,
		    eco,
		    ending,
		    result) != 11) {
			fprintf(stderr, "FICS: Error in journal info format. "
			    "%s\n", fname);
			fclose(fp);
			return COM_OK;
		}

		WhiteName[13]	= '\0'; // only first 13 chars in name
		BlackName[13]	= '\0';

		pprintf(p, "%c: %-13s %4d    %-13s %4d    [%3s%3d%4d] %s %3s "
		    "%-7s\n",
		    count, WhiteName, WhiteRating,
		    BlackName, BlackRating,
		    type, (t / 600), (i / 10), eco, ending,
		    result);
	}

	fclose(fp);
	return COM_OK;
}

PUBLIC int
pgames(int p, int p1, char *fname)
{
	FILE	*fp;
	char	 MyColor[2] = { 0,0 };
	char	 OppName[MAX_LOGIN_NAME + 1] = { '\0' };
	char	 eco[100] = { '\0' };
	char	 ending[100] = { '\0' };
	char	 result[2] = { 0,0 }; // XXX: right size?
	char	 type[100] = { '\0' };
	int	 MyRating, OppRating;
	int	 count;
	int	 wt, wi, bt, bi;
	time_t	 t;

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

	pprintf(p, "History for %s:\n", parray[p1].name);
	pprintf(p, "                  Opponent      Type         "
	    "ECO End Date\n");

	_Static_assert(ARRAY_SIZE(result) > 1,   "'result' too small");
	_Static_assert(ARRAY_SIZE(MyColor) > 1,  "'MyColor' too small");
	_Static_assert(ARRAY_SIZE(OppName) > 19, "'OppName' too small");
	_Static_assert(ARRAY_SIZE(type) > 99,    "'type' too small");
	_Static_assert(ARRAY_SIZE(eco) > 99,     "'eco' too small");
	_Static_assert(ARRAY_SIZE(ending) > 99,  "'ending' too small");

	while (!feof(fp)) {
		if (fscanf(fp, "%d %1s %d %1s %d %19s %99s %d %d %d %d %99s "
		    "%99s %ld\n",
		    &count, result, &MyRating, MyColor,
		    &OppRating, OppName,
		    type,
		    &wt, &wi,
		    &bt, &bi,
		    eco,
		    ending,
		    (long int *)&t) != 14) {
			fprintf(stderr, "FICS: Error in games info format. "
			    "%s\n", fname);
			fclose(fp);
			return COM_OK;
		}

		OppName[13] = '\0'; // only first 13 chars in name

		pprintf(p, "%2d: %s %4d %s %4d %-13s [%3s%3d%4d] %s %3s %s",
		    count, result, MyRating, MyColor,
		    OppRating, OppName,
		    type, (wt / 600), (wi / 10), eco, ending,
		    ctime(&t));
	}

	fclose(fp);
	return COM_OK;
}

PUBLIC void
game_write_complete(int g, int isDraw, char *EndSymbol)
{
	FILE	*fp;
	char	 fname[MAX_FILENAME_SIZE] = { '\0' };
	int	 fd;
	int	 wp = garray[g].white, bp = garray[g].black;
	time_t	 now = time(NULL);

	do {
		msnprintf(fname, sizeof fname, "%s/%ld/%ld",
		    hist_dir,
		    (long int)(now % 100),
		    (long int)now);
		fd = open(fname, (O_WRONLY | O_CREAT | O_EXCL), 0644);
		if (fd == EEXIST)
			now++;
	} while (fd == EEXIST);

	if (fd >= 0) {
		fp = fdopen(fd, "w");
		if (fp != NULL)
			WriteGameFile(fp, g);
		else {
			fprintf(stderr, "Trouble writing history file %s",
			    fname);
		}
		fclose(fp);
		close(fd);
	}

	msnprintf(fname, sizeof fname, "%s/player_data/%c/%s.%s",
	    stats_dir,
	    parray[wp].login[0],
	    parray[wp].login,
	    STATS_GAMES);
	write_g_out(g, fname, 10, isDraw, EndSymbol, parray[wp].name, &now);

	msnprintf(fname, sizeof fname, "%s/player_data/%c/%s.%s",
	    stats_dir,
	    parray[bp].login[0],
	    parray[bp].login,
	    STATS_GAMES);
	write_g_out(g, fname, 10, isDraw, EndSymbol, parray[bp].name, &now);
}

PUBLIC int
game_count(void)
{
	int	g, count = 0;

	for (g = 0; g < g_num; g++) {
		if (garray[g].status == GAME_ACTIVE ||
		    garray[g].status == GAME_EXAMINE)
			count++;
	}

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