/*
 * Copyright (c) 1983-2003, Regents of the University of California.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * + Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 * + Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 * + Neither the name of the University of California, San Francisco nor
 *   the names of its contributors may be used to endorse or promote
 *   products derived from this software without specific prior written
 *   permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * $OpenBSD: driver.c,v 1.17 2007/04/02 14:55:16 jmc Exp $
 * $NetBSD: driver.c,v 1.5 1997/10/20 00:37:16 lukem Exp $
 * $DragonFly: src/games/hunt/huntd/driver.c,v 1.3 2008/11/10 15:28:13 swildner Exp $
 */

#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/socket.h>

#include <netinet/in.h>
#include <arpa/inet.h>

#include <err.h>
#include <errno.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#include <tcpd.h>
#include <syslog.h>
#include <netdb.h>
#include <paths.h>
#include <fcntl.h>
#include "hunt.h"
#include "conf.h"
#include "server.h"

char	*First_arg;		/* pointer to argv[0] */
u_int16_t Server_port;
int	Server_socket;		/* test socket to answer datagrams */
FLAG	should_announce = TRUE;	/* true if listening on standard port */
u_short	sock_port;		/* port # of tcp listen socket */
u_short	stat_port;		/* port # of statistics tcp socket */
in_addr_t Server_addr = INADDR_ANY;	/* address to bind to */

static	void	clear_scores(void);
static	int	havechar(PLAYER *);
static	void	init(void);
	int	main(int, char *[]);
static	void	makeboots(void);
static	void	send_stats(void);
static	void	zap(PLAYER *, FLAG);
static  void	announce_game(void);
static	void	siginfo(int);
static	void	print_stats(FILE *);
static	void	handle_wkport(int);

/*
 * main:
 *	The main program.
 */
int
main(int ac, char **av)
{
	PLAYER		*pp;
	int		had_char;
	static fd_set	read_fds;
	static FLAG	first = TRUE;
	static FLAG	server = FALSE;
	int		c;
	static struct timeval	linger = { 0, 0 };
	static struct timeval	timeout = { 0, 0 }, *to;
	struct spawn	*sp, *spnext;
	int		ret;
	int		nready;
	int		fd;

	First_arg = av[0];

	config();

	while ((c = getopt(ac, av, "sp:a:D:")) != -1) {
		switch (c) {
		  case 's':
			server = TRUE;
			break;
		  case 'p':
			should_announce = FALSE;
			Server_port = atoi(optarg);
			break;
		  case 'a':
			if (!inet_aton(optarg, (struct in_addr *)&Server_addr))
				err(1, "bad interface address: %s", optarg);
			break;
		  case 'D':
			config_arg(optarg);
			break;
		  default:
erred:
			fprintf(stderr,
			    "usage: %s [-s] [-a addr] [-Dvar=value ...] "
			    "[-p port]\n",
			    av[0]);
			exit(2);
		}
	}
	if (optind < ac)
		goto erred;

	/* Open syslog: */
	openlog("huntd", LOG_PID | (conf_logerr && !server? LOG_PERROR : 0),
		LOG_DAEMON);

	/* Initialise game parameters: */
	init();

again:
	do {
		/* First, poll to see if we can get input */
		do {
			read_fds = Fds_mask;
			errno = 0;
			timerclear(&timeout);
			nready = select(Num_fds, &read_fds, NULL, NULL,
			    &timeout);
			if (nready < 0 && errno != EINTR) {
				logit(LOG_ERR, "select");
				cleanup(1);
			}
		} while (nready < 0);

		if (nready == 0) {
			/*
			 * Nothing was ready. We do some work now
			 * to see if the simulation has any pending work
			 * to do, and decide if we need to block
			 * indefinitely or just timeout.
			 */
			do {
				if (conf_simstep && can_moveshots()) {
				/*
				 * block for a short time before continuing
				 * with explosions, bullets and whatnot
				 */
					to = &timeout;
					to->tv_sec =  conf_simstep / 1000000;
					to->tv_usec = conf_simstep % 1000000;
				} else
				/*
				 * since there's nothing going on,
				 * just block waiting for external activity
				 */
					to = NULL;

				read_fds = Fds_mask;
				errno = 0;
				nready = select(Num_fds, &read_fds, NULL, NULL,
				    to);
				if (nready < 0 && errno != EINTR) {
					logit(LOG_ERR, "select");
					cleanup(1);
				}
			} while (nready < 0);
		}

		/* Remember which descriptors are active: */
		Have_inp = read_fds;

		/* Answer new player connections: */
		if (FD_ISSET(Socket, &Have_inp))
			answer_first();

		/* Continue answering new player connections: */
		for (sp = Spawn; sp; ) {
			spnext = sp->next;
			fd = sp->fd;
			if (FD_ISSET(fd, &Have_inp) && answer_next(sp)) {
				/*
				 * Remove from the spawn list. (fd remains in
				 * read set).
				 */
				*sp->prevnext = sp->next;
				if (sp->next)
					sp->next->prevnext = sp->prevnext;
				free(sp);

				/* We probably consumed all data. */
				FD_CLR(fd, &Have_inp);

				/* Announce game if this is the first spawn. */
				if (first && should_announce)
					announce_game();
				first = FALSE;
			}
			sp = spnext;
		}

		/* Process input and move bullets until we've exhausted input */
		had_char = TRUE;
		while (had_char) {

			moveshots();
			for (pp = Player; pp < End_player; )
				if (pp->p_death[0] != '\0')
					zap(pp, TRUE);
				else
					pp++;
			for (pp = Monitor; pp < End_monitor; )
				if (pp->p_death[0] != '\0')
					zap(pp, FALSE);
				else
					pp++;

			had_char = FALSE;
			for (pp = Player; pp < End_player; pp++)
				if (havechar(pp)) {
					execute(pp);
					pp->p_nexec++;
					had_char = TRUE;
				}
			for (pp = Monitor; pp < End_monitor; pp++)
				if (havechar(pp)) {
					mon_execute(pp);
					pp->p_nexec++;
					had_char = TRUE;
				}
		}

		/* Handle a datagram sent to the server socket: */
		if (FD_ISSET(Server_socket, &Have_inp))
			handle_wkport(Server_socket);

		/* Answer statistics connections: */
		if (FD_ISSET(Status, &Have_inp))
			send_stats();

		/* Flush/synchronize all the displays: */
		for (pp = Player; pp < End_player; pp++) {
			if (FD_ISSET(pp->p_fd, &read_fds)) {
				sendcom(pp, READY, pp->p_nexec);
				pp->p_nexec = 0;
			}
			flush(pp);
		}
		for (pp = Monitor; pp < End_monitor; pp++) {
			if (FD_ISSET(pp->p_fd, &read_fds)) {
				sendcom(pp, READY, pp->p_nexec);
				pp->p_nexec = 0;
			}
			flush(pp);
		}
	} while (Nplayer > 0);

	/* No more players! */

	/* No players yet or a continuous game? */
	if (first || conf_linger < 0)
		goto again;

	/* Wait a short while for one to come back: */
	read_fds = Fds_mask;
	linger.tv_sec = conf_linger;
	while ((ret = select(Num_fds, &read_fds, NULL, NULL, &linger)) < 0) {
		if (errno != EINTR) {
			logit(LOG_WARNING, "select");
			break;
		}
		read_fds = Fds_mask;
		linger.tv_sec = conf_linger;
		linger.tv_usec = 0;
	}
	if (ret > 0)
		/* Someone returned! Resume the game: */
		goto again;
	/* else, it timed out, and the game is really over. */

	/* If we are an inetd server, we should re-init the map and restart: */
	if (server) {
		clear_scores();
		makemaze();
		clearwalls();
		makeboots();
		first = TRUE;
		goto again;
	}

	/* Get rid of any attached monitors: */
	for (pp = Monitor; pp < End_monitor; )
		zap(pp, FALSE);

	/* Fin: */
	cleanup(0);
	exit(0);
}

/*
 * init:
 *	Initialize the global parameters.
 */
static void
init(void)
{
	int	i;
	struct sockaddr_in	test_port;
	int	true = 1;
	socklen_t	len;
	struct sockaddr_in	addr;
	struct sigaction	sact;
	struct servent *se;

	(void) setsid();
	if (setpgid(getpid(), getpid()) == -1)
		err(1, "setpgid");

	sact.sa_flags = SA_RESTART;
	sigemptyset(&sact.sa_mask);

	/* Ignore HUP, QUIT and PIPE: */
	sact.sa_handler = SIG_IGN;
	if (sigaction(SIGHUP, &sact, NULL) == -1)
		err(1, "sigaction SIGHUP");
	if (sigaction(SIGQUIT, &sact, NULL) == -1)
		err(1, "sigaction SIGQUIT");
	if (sigaction(SIGPIPE, &sact, NULL) == -1)
		err(1, "sigaction SIGPIPE");

	/* Clean up gracefully on INT and TERM: */
	sact.sa_handler = cleanup;
	if (sigaction(SIGINT, &sact, NULL) == -1)
		err(1, "sigaction SIGINT");
	if (sigaction(SIGTERM, &sact, NULL) == -1)
		err(1, "sigaction SIGTERM");

	/* Handle INFO: */
	sact.sa_handler = siginfo;
	if (sigaction(SIGINFO, &sact, NULL) == -1)
		err(1, "sigaction SIGINFO");

	if (chdir("/") == -1)
		warn("chdir");
	(void) umask(0777);

	/* Initialize statistics socket: */
	addr.sin_family = AF_INET;
	addr.sin_addr.s_addr = Server_addr;
	addr.sin_port = 0;

	Status = socket(AF_INET, SOCK_STREAM, 0);
	if (bind(Status, (struct sockaddr *) &addr, sizeof addr) < 0) {
		logit(LOG_ERR, "bind");
		cleanup(1);
	}
	if (listen(Status, 5) == -1) {
		logit(LOG_ERR, "listen");
		cleanup(1);
	}

	len = sizeof (struct sockaddr_in);
	if (getsockname(Status, (struct sockaddr *) &addr, &len) < 0)  {
		logit(LOG_ERR, "getsockname");
		cleanup(1);
	}
	stat_port = ntohs(addr.sin_port);

	/* Initialize main socket: */
	addr.sin_family = AF_INET;
	addr.sin_addr.s_addr = Server_addr;
	addr.sin_port = 0;

	Socket = socket(AF_INET, SOCK_STREAM, 0);

	if (bind(Socket, (struct sockaddr *) &addr, sizeof addr) < 0) {
		logit(LOG_ERR, "bind");
		cleanup(1);
	}
	if (listen(Socket, 5) == -1) {
		logit(LOG_ERR, "listen");
		cleanup(1);
	}

	len = sizeof (struct sockaddr_in);
	if (getsockname(Socket, (struct sockaddr *) &addr, &len) < 0)  {
		logit(LOG_ERR, "getsockname");
		cleanup(1);
	}
	sock_port = ntohs(addr.sin_port);

	/* Initialize minimal select mask */
	FD_ZERO(&Fds_mask);
	FD_SET(Socket, &Fds_mask);
	FD_SET(Status, &Fds_mask);
	Num_fds = ((Socket > Status) ? Socket : Status) + 1;

	/* Find the port that huntd should run on */
	if (Server_port == 0) {
		se = getservbyname("hunt", "udp");
		if (se != NULL)
			Server_port = ntohs(se->s_port);
		else
			Server_port = HUNT_PORT;
	}

	/* Check if stdin is a socket: */
	len = sizeof (struct sockaddr_in);
	if (getsockname(STDIN_FILENO, (struct sockaddr *) &test_port, &len) >= 0
	    && test_port.sin_family == AF_INET) {
		/* We are probably running from inetd:  don't log to stderr */
		Server_socket = STDIN_FILENO;
		conf_logerr = 0;
		if (test_port.sin_port != htons((u_short) Server_port)) {
			/* Private game */
			should_announce = FALSE;
			Server_port = ntohs(test_port.sin_port);
		}
	} else {
		/* We need to listen on a socket: */
		test_port = addr;
		test_port.sin_port = htons((u_short) Server_port);

		Server_socket = socket(AF_INET, SOCK_DGRAM, 0);

		/* Permit multiple huntd's on the same port. */
		if (setsockopt(Server_socket, SOL_SOCKET, SO_REUSEPORT, &true,
		    sizeof true) < 0)
			logit(LOG_ERR, "setsockopt SO_REUSEADDR");

		if (bind(Server_socket, (struct sockaddr *) &test_port,
		    sizeof test_port) < 0) {
			logit(LOG_ERR, "bind port %d", Server_port);
			cleanup(1);
		}

		/* Datagram sockets do not need a listen() call. */
	}

	/* We'll handle the broadcast listener in the main loop: */
	FD_SET(Server_socket, &Fds_mask);
	if (Server_socket + 1 > Num_fds)
		Num_fds = Server_socket + 1;

	/* Initialise the random seed: */
	srandomdev();

	/* Dig the maze: */
	makemaze();

	/* Create some boots, if needed: */
	makeboots();

	/* Construct a table of what objects a player can see over: */
	for (i = 0; i < NASCII; i++)
		See_over[i] = TRUE;
	See_over[DOOR] = FALSE;
	See_over[WALL1] = FALSE;
	See_over[WALL2] = FALSE;
	See_over[WALL3] = FALSE;
	See_over[WALL4] = FALSE;
	See_over[WALL5] = FALSE;

	logx(LOG_INFO, "game started");
}

/*
 * makeboots:
 *	Put the boots in the maze
 */
static void
makeboots(void)
{
	int	x, y;
	PLAYER	*pp;

	if (conf_boots) {
		do {
			x = rand_num(WIDTH - 1) + 1;
			y = rand_num(HEIGHT - 1) + 1;
		} while (Maze[y][x] != SPACE);
		Maze[y][x] = BOOT_PAIR;
	}

	for (pp = Boot; pp < &Boot[NBOOTS]; pp++)
		pp->p_flying = -1;
}


/*
 * checkdam:
 *	Apply damage to the victim from an attacker.
 *	If the victim dies as a result, give points to 'credit',
 */
void
checkdam(PLAYER *victim, PLAYER *attacker, IDENT *credit, int damage,
    char stype)
{
	const char	*cp;
	int	y;

	/* Don't do anything if the victim is already in the throes of death */
	if (victim->p_death[0] != '\0')
		return;

	/* Weaken slime attacks by 0.5 * number of boots the victim has on: */
	if (stype == SLIME)
		switch (victim->p_nboots) {
		  default:
			break;
		  case 1:
			damage = (damage + 1) / 2;
			break;
		  case 2:
			if (attacker != NULL)
				message(attacker, "He has boots on!");
			return;
		}

	/* The victim sustains some damage: */
	victim->p_damage += damage;

	/* Check if the victim survives the hit: */
	if (victim->p_damage <= victim->p_damcap) {
		/* They survive. */
		outyx(victim, STAT_DAM_ROW, STAT_VALUE_COL, "%2d",
			victim->p_damage);
		return;
	}

	/* Describe how the victim died: */
	switch (stype) {
	  default:
		cp = "Killed";
		break;
	  case FALL:
		cp = "Killed on impact";
		break;
	  case KNIFE:
		cp = "Stabbed to death";
		victim->p_ammo = 0;		/* No exploding */
		break;
	  case SHOT:
		cp = "Shot to death";
		break;
	  case GRENADE:
	  case SATCHEL:
	  case BOMB:
		cp = "Bombed";
		break;
	  case MINE:
	  case GMINE:
		cp = "Blown apart";
		break;
	  case SLIME:
		cp = "Slimed";
		if (credit != NULL)
			credit->i_slime++;
		break;
	  case LAVA:
		cp = "Baked";
		break;
	  case DSHOT:
		cp = "Eliminated";
		break;
	}

	if (credit == NULL) {
		const char *blame;

		/*
		 * Nobody is taking the credit for the kill.
		 * Attribute it to either a mine or 'act of God'.
		 */
		switch (stype) {
		case MINE:
		case GMINE:
			blame = "a mine";
			break;
		default:
			blame = "act of God";
			break;
		}

		/* Set the death message: */
		(void) snprintf(victim->p_death, sizeof victim->p_death,
			"| %s by %s |", cp, blame);

		/* No further score crediting needed. */
		return;
	}

	/* Set the death message: */
	(void) snprintf(victim->p_death, sizeof victim->p_death,
		"| %s by %s |", cp, credit->i_name);

	if (victim == attacker) {
		/* No use killing yourself. */
		credit->i_kills--;
		credit->i_bkills++;
	}
	else if (victim->p_ident->i_team == ' '
	    || victim->p_ident->i_team != credit->i_team) {
		/* A cross-team kill: */
		credit->i_kills++;
		credit->i_gkills++;
	}
	else {
		/* They killed someone on the same team: */
		credit->i_kills--;
		credit->i_bkills++;
	}

	/* Compute the new credited score: */
	credit->i_score = credit->i_kills / (double) credit->i_entries;

	/* The victim accrues one death: */
	victim->p_ident->i_deaths++;

	/* Account for 'Stillborn' deaths */
	if (victim->p_nchar == 0)
		victim->p_ident->i_stillb++;

	if (attacker) {
		/* Give the attacker player a bit more strength */
		attacker->p_damcap += conf_killgain;
		attacker->p_damage -= conf_killgain;
		if (attacker->p_damage < 0)
			attacker->p_damage = 0;

		/* Tell the attacker his new strength: */
		outyx(attacker, STAT_DAM_ROW, STAT_VALUE_COL, "%2d/%2d",
			attacker->p_damage, attacker->p_damcap);

		/* Tell the attacker his new 'kill count': */
		outyx(attacker, STAT_KILL_ROW, STAT_VALUE_COL, "%3d",
			(attacker->p_damcap - conf_maxdam) / 2);

		/* Update the attacker's score for everyone else */
		y = STAT_PLAY_ROW + 1 + (attacker - Player);
		outyx(ALL_PLAYERS, y, STAT_NAME_COL,
			"%5.2f", attacker->p_ident->i_score);
	}
}

/*
 * zap:
 *	Kill off a player and take them out of the game.
 *	The 'was_player' flag indicates that the player was not
 *	a monitor and needs extra cleaning up.
 */
static void
zap(PLAYER *pp, FLAG was_player)
{
	int	len;
	BULLET	*bp;
	PLAYER	*np;
	int	x, y;
	int	savefd;

	if (was_player) {
		/* If they died from a shot, clean up shrapnel */
		if (pp->p_undershot)
			fixshots(pp->p_y, pp->p_x, pp->p_over);
		/* Let the player see their last position: */
		drawplayer(pp, FALSE);
		/* Remove from game: */
		Nplayer--;
	}

	/* Display the cause of death in the centre of the screen: */
	len = strlen(pp->p_death);
	x = (WIDTH - len) / 2;
	outyx(pp, HEIGHT / 2, x, "%s", pp->p_death);

	/* Put some horizontal lines around and below the death message: */
	memset(pp->p_death + 1, '-', len - 2);
	pp->p_death[0] = '+';
	pp->p_death[len - 1] = '+';
	outyx(pp, HEIGHT / 2 - 1, x, "%s", pp->p_death);
	outyx(pp, HEIGHT / 2 + 1, x, "%s", pp->p_death);

	/* Move to bottom left */
	cgoto(pp, HEIGHT, 0);

	savefd = pp->p_fd;

	if (was_player) {
		int	expl_charge;
		int	expl_type;
		int	ammo_exploding;

		/* Check all the bullets: */
		for (bp = Bullets; bp != NULL; bp = bp->b_next) {
			if (bp->b_owner == pp)
				/* Zapped players can't own bullets: */
				bp->b_owner = NULL;
			if (bp->b_x == pp->p_x && bp->b_y == pp->p_y)
				/* Bullets over the player are now over air: */
				bp->b_over = SPACE;
		}

		/* Explode a random fraction of the player's ammo: */
		ammo_exploding = rand_num(pp->p_ammo);

		/* Determine the type and amount of detonation: */
		expl_charge = rand_num(ammo_exploding + 1);
		if (pp->p_ammo == 0)
			/* Ignore the no-ammo case: */
			expl_charge = expl_type = 0;
		else if (ammo_exploding >= pp->p_ammo - 1) {
			/* Maximal explosions always appear as slime: */
			expl_charge = pp->p_ammo;
			expl_type = SLIME;
		} else {
			/*
			 * Figure out the best effective explosion
			 * type to use, given the amount of charge
			 */
			int btype, stype;
			for (btype = MAXBOMB - 1; btype > 0; btype--)
				if (expl_charge >= shot_req[btype])
					break;
			for (stype = MAXSLIME - 1; stype > 0; stype--)
				if (expl_charge >= slime_req[stype])
					break;
			/* Pick the larger of the bomb or slime: */
			if (btype >= 0 && stype >= 0) {
				if (shot_req[btype] > slime_req[btype])
					btype = -1;
			}
			if (btype >= 0)  {
				expl_type = shot_type[btype];
				expl_charge = shot_req[btype];
			} else
				expl_type = SLIME;
		}

		if (expl_charge > 0) {
			char buf[BUFSIZ];

			/* Detonate: */
			(void) add_shot(expl_type, pp->p_y, pp->p_x,
			    pp->p_face, expl_charge, NULL,
			    TRUE, SPACE);

			/* Explain what the explosion is about. */
			snprintf(buf, sizeof buf, "%s detonated.",
				pp->p_ident->i_name);
			message(ALL_PLAYERS, buf);

			while (pp->p_nboots-- > 0) {
				/* Throw one of the boots away: */
				for (np = Boot; np < &Boot[NBOOTS]; np++)
					if (np->p_flying < 0)
						break;
#ifdef DIAGNOSTIC
				if (np >= &Boot[NBOOTS])
					err(1, "Too many boots");
#endif
				/* Start the boots from where the player is */
				np->p_undershot = FALSE;
				np->p_x = pp->p_x;
				np->p_y = pp->p_y;
				/* Throw for up to 20 steps */
				np->p_flying = rand_num(20);
				np->p_flyx = 2 * rand_num(6) - 5;
				np->p_flyy = 2 * rand_num(6) - 5;
				np->p_over = SPACE;
				np->p_face = BOOT;
				showexpl(np->p_y, np->p_x, BOOT);
			}
		}
		/* No explosion. Leave the player's boots behind. */
		else if (pp->p_nboots > 0) {
			if (pp->p_nboots == 2)
				Maze[pp->p_y][pp->p_x] = BOOT_PAIR;
			else
				Maze[pp->p_y][pp->p_x] = BOOT;
			if (pp->p_undershot)
				fixshots(pp->p_y, pp->p_x,
					Maze[pp->p_y][pp->p_x]);
		}

		/* Any unexploded ammo builds up in the volcano: */
		volcano += pp->p_ammo - expl_charge;

		/* Volcano eruption: */
		if (conf_volcano && rand_num(100) < volcano /
		    conf_volcano_max) {
			/* Erupt near the middle of the map */
			do {
				x = rand_num(WIDTH / 2) + WIDTH / 4;
				y = rand_num(HEIGHT / 2) + HEIGHT / 4;
			} while (Maze[y][x] != SPACE);

			/* Convert volcano charge into lava: */
			(void) add_shot(LAVA, y, x, LEFTS, volcano,
				NULL, TRUE, SPACE);
			volcano = 0;

			/* Tell eveyone what's happening */
			message(ALL_PLAYERS, "Volcano eruption.");
		}

		/* Drone: */
		if (conf_drone && rand_num(100) < 2) {
			/* Find a starting place near the middle of the map: */
			do {
				x = rand_num(WIDTH / 2) + WIDTH / 4;
				y = rand_num(HEIGHT / 2) + HEIGHT / 4;
			} while (Maze[y][x] != SPACE);

			/* Start the drone going: */
			add_shot(DSHOT, y, x, rand_dir(),
				shot_req[conf_mindshot +
				rand_num(MAXBOMB - conf_mindshot)],
				NULL, FALSE, SPACE);
		}

		/* Tell the zapped player's client to shut down. */
		sendcom(pp, ENDWIN, ' ');
		(void) fclose(pp->p_output);

		/* Close up the gap in the Player array: */
		End_player--;
		if (pp != End_player) {
			/* Move the last player into the gap: */
			memcpy(pp, End_player, sizeof *pp);
			outyx(ALL_PLAYERS,
				STAT_PLAY_ROW + 1 + (pp - Player),
				STAT_NAME_COL,
				"%5.2f%c%-10.10s %c",
				pp->p_ident->i_score, stat_char(pp),
				pp->p_ident->i_name, pp->p_ident->i_team);
		}

		/* Erase the last player from the display: */
		cgoto(ALL_PLAYERS, STAT_PLAY_ROW + 1 + Nplayer, STAT_NAME_COL);
		ce(ALL_PLAYERS);
	}
	else {
		/* Zap a monitor */

		/* Close the session: */
		sendcom(pp, ENDWIN, LAST_PLAYER);
		(void) fclose(pp->p_output);

		/* shuffle the monitor table */
		End_monitor--;
		if (pp != End_monitor) {
			memcpy(pp, End_monitor, sizeof *pp);
			outyx(ALL_PLAYERS,
				STAT_MON_ROW + 1 + (pp - Player), STAT_NAME_COL,
				"%5.5s %-10.10s %c", " ",
				pp->p_ident->i_name, pp->p_ident->i_team);
		}

		/* Erase the last monitor in the list */
		cgoto(ALL_PLAYERS,
			STAT_MON_ROW + 1 + (End_monitor - Monitor),
			STAT_NAME_COL);
		ce(ALL_PLAYERS);
	}

	/* Update the file descriptor sets used by select: */
	FD_CLR(savefd, &Fds_mask);
	if (Num_fds == savefd + 1) {
		Num_fds = Socket;
		if (Server_socket > Socket)
			Num_fds = Server_socket;
		for (np = Player; np < End_player; np++)
			if (np->p_fd > Num_fds)
				Num_fds = np->p_fd;
		for (np = Monitor; np < End_monitor; np++)
			if (np->p_fd > Num_fds)
				Num_fds = np->p_fd;
		Num_fds++;
	}
}

/*
 * rand_num:
 *	Return a random number in a given range.
 */
int
rand_num(int range)
{
	if (range == 0)
		return 0;
	return (random() % range);
}

/*
 * havechar:
 *	Check to see if we have any characters in the input queue; if
 *	we do, read them, stash them away, and return TRUE; else return
 *	FALSE.
 */
static int
havechar(PLAYER *pp)
{
	int ret;

	/* Do we already have characters? */
	if (pp->p_ncount < pp->p_nchar)
		return TRUE;
	/* Ignore if nothing to read. */
	if (!FD_ISSET(pp->p_fd, &Have_inp))
		return FALSE;
	/* Remove the player from the read set until we have drained them: */
	FD_CLR(pp->p_fd, &Have_inp);

	/* Suck their keypresses into a buffer: */
check_again:
	errno = 0;
	ret = read(pp->p_fd, pp->p_cbuf, sizeof pp->p_cbuf);
	if (ret == -1) {
		if (errno == EINTR)
			goto check_again;
		if (errno == EAGAIN) {
#ifdef DEBUG
			warn("Have_inp is wrong for %d", pp->p_fd);
#endif
			return FALSE;
		}
		logit(LOG_INFO, "read");
	}
	if (ret > 0) {
		/* Got some data */
		pp->p_nchar = ret;
	} else {
		/* Connection was lost/closed: */
		pp->p_cbuf[0] = 'q';
		pp->p_nchar = 1;
	}
	/* Reset pointer into read buffer */
	pp->p_ncount = 0;
	return TRUE;
}

/*
 * cleanup:
 *	Exit with the given value, cleaning up any droppings lying around
 */
void
cleanup(int eval)
{
	PLAYER	*pp;

	/* Place their cursor in a friendly position: */
	cgoto(ALL_PLAYERS, HEIGHT, 0);

	/* Send them all the ENDWIN command: */
	sendcom(ALL_PLAYERS, ENDWIN, LAST_PLAYER);

	/* And close their connections: */
	for (pp = Player; pp < End_player; pp++)
		(void) fclose(pp->p_output);
	for (pp = Monitor; pp < End_monitor; pp++)
		(void) fclose(pp->p_output);

	/* Close the server socket: */
	(void) close(Socket);

	/* The end: */
	logx(LOG_INFO, "game over");
	exit(eval);
}

/*
 * send_stats:
 *	Accept a connection to the statistics port, and emit
 *	the stats.
 */
static void
send_stats(void)
{
	FILE	*fp;
	int	s;
	struct sockaddr_in	sockstruct;
	socklen_t	socklen;
	struct request_info ri;
	int	flags;

	/* Accept a connection to the statistics socket: */
	socklen = sizeof sockstruct;
	s = accept(Status, (struct sockaddr *) &sockstruct, &socklen);
	if (s < 0) {
		if (errno == EINTR)
			return;
		logx(LOG_ERR, "accept");
		return;
	}

	/* Check for access permissions: */
	request_init(&ri, RQ_DAEMON, "huntd", RQ_FILE, s, 0);
	fromhost(&ri);
	if (hosts_access(&ri) == 0) {
		logx(LOG_INFO, "rejected connection from %s", eval_client(&ri));
		close(s);
		return;
	}

	/* Don't allow the writes to block: */
	flags = fcntl(s, F_GETFL, 0);
	flags |= O_NDELAY;
	(void) fcntl(s, F_SETFL, flags);

	fp = fdopen(s, "w");
	if (fp == NULL) {
		logit(LOG_ERR, "fdopen");
		(void) close(s);
		return;
	}

	print_stats(fp);

	(void) fclose(fp);
}

/*
 * print_stats:
 * 	emit the game statistics
 */
void
print_stats(FILE *fp)
{
	IDENT	*ip;
	PLAYER  *pp;

	/* Send the statistics as raw text down the socket: */
	fputs("Name\t\tScore\tDucked\tAbsorb\tFaced\tShot\tRobbed\tMissed\tSlimeK\n", fp);
	for (ip = Scores; ip != NULL; ip = ip->i_next) {
		fprintf(fp, "%s%c%c%c\t", ip->i_name,
			ip->i_team == ' ' ? ' ' : '[',
			ip->i_team,
			ip->i_team == ' ' ? ' ' : ']'
		);
		if (strlen(ip->i_name) + 3 < 8)
			putc('\t', fp);
		fprintf(fp, "%.2f\t%d\t%d\t%d\t%d\t%d\t%d\t%d\n",
			ip->i_score, ip->i_ducked, ip->i_absorbed,
			ip->i_faced, ip->i_shot, ip->i_robbed,
			ip->i_missed, ip->i_slime);
	}
	fputs("\n\nName\t\tEnemy\tFriend\tDeaths\tStill\tSaved\tConnect\n", fp);
	for (ip = Scores; ip != NULL; ip = ip->i_next) {
		fprintf(fp, "%s%c%c%c\t", ip->i_name,
			ip->i_team == ' ' ? ' ' : '[',
			ip->i_team,
			ip->i_team == ' ' ? ' ' : ']'
		);
		if (strlen(ip->i_name) + 3 < 8)
			putc('\t', fp);
		fprintf(fp, "%d\t%d\t%d\t%d\t%d\t",
			ip->i_gkills, ip->i_bkills, ip->i_deaths,
			ip->i_stillb, ip->i_saved);
		for (pp = Player; pp < End_player; pp++)
			if (pp->p_ident == ip)
				putc('p', fp);
		for (pp = Monitor; pp < End_monitor; pp++)
			if (pp->p_ident == ip)
				putc('m', fp);
		putc('\n', fp);
	}
}


/*
 * Send the game statistics to the controlling tty
 */
static void
siginfo(int sig __unused)
{
	int tty;
	FILE *fp;

	if ((tty = open(_PATH_TTY, O_WRONLY)) >= 0) {
		fp = fdopen(tty, "w");
		print_stats(fp);
		answer_info(fp);
		fclose(fp);
	}
}

/*
 * clear_scores:
 *	Clear the Scores list.
 */
static void
clear_scores(void)
{
	IDENT	*ip, *nextip;

	/* Release the list of scores: */
	for (ip = Scores; ip != NULL; ip = nextip) {
		nextip = ip->i_next;
		free((char *) ip);
	}
	Scores = NULL;
}

/*
 * announce_game:
 *	Publically announce the game
 */
static void
announce_game(void)
{

	/* TODO: could use system() to do something user-configurable */
}

/*
 * Handle a UDP packet sent to the well known port.
 */
static void
handle_wkport(int fd)
{
	struct sockaddr		fromaddr;
	socklen_t		fromlen;
	u_int16_t		query;
	u_int16_t		response;
	struct request_info	ri;

	request_init(&ri, RQ_DAEMON, "huntd", RQ_FILE, fd, 0);
	fromhost(&ri);
	fromlen = sizeof fromaddr;
	if (recvfrom(fd, &query, sizeof query, 0, &fromaddr, &fromlen) == -1)
	{
		logit(LOG_WARNING, "recvfrom");
		return;
	}

#ifdef DEBUG
	fprintf(stderr, "query %d (%s) from %s:%d\n", query,
		query == C_MESSAGE ? "C_MESSAGE" :
		query == C_SCORES ? "C_SCORES" :
		query == C_PLAYER ? "C_PLAYER" :
		query == C_MONITOR ? "C_MONITOR" : "?",
		inet_ntoa(((struct sockaddr_in *)&fromaddr)->sin_addr),
		ntohs(((struct sockaddr_in *)&fromaddr)->sin_port));
#endif

	/* Do we allow access? */
	if (hosts_access(&ri) == 0) {
		logx(LOG_INFO, "rejected connection from %s", eval_client(&ri));
		return;
	}

	query = ntohs(query);

	switch (query) {
	  case C_MESSAGE:
		if (Nplayer <= 0)
			/* Don't bother replying if nobody to talk to: */
			return;
		/* Return the number of people playing: */
		response = Nplayer;
		break;
	  case C_SCORES:
		/* Someone wants the statistics port: */
		response = stat_port;
		break;
	  case C_PLAYER:
	  case C_MONITOR:
		/* Someone wants to play or watch: */
		if (query == C_MONITOR && Nplayer <= 0)
			/* Don't bother replying if there's nothing to watch: */
			return;
		/* Otherwise, tell them how to get to the game: */
		response = sock_port;
		break;
	  default:
		logit(LOG_INFO, "unknown udp query %d", query);
		return;
	}

	response = ntohs(response);
	if (sendto(fd, &response, sizeof response, 0,
	    &fromaddr, sizeof fromaddr) == -1)
		logit(LOG_WARNING, "sendto");
}
