/************************************************************************
 * serverside.c   Handles the server side of dopewars                   *
 * Copyright (C)  1998-2022  Ben Webb                                   *
 *                Email: benwebb@users.sf.net                           *
 *                WWW: https://dopewars.sourceforge.io/                 *
 *                                                                      *
 * 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.                         *
 *                                                                      *
 * You should have received a copy of the GNU General Public License    *
 * along with this program; if not, write to the Free Software          *
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston,               *
 *                   MA  02111-1307, USA.                               *
 ************************************************************************/

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <stdio.h>
#include <string.h>
#include <sys/types.h>          /* For size_t etc. */
#include <sys/stat.h>

#ifdef CYGWIN
#include <winsock2.h>           /* For network functions */
#include <windows.h>            /* For datatypes such as BOOL */
#include <process.h>            /* For getpid */
#else
#include <sys/socket.h>         /* For struct sockaddr etc. */
#include <netinet/in.h>         /* For struct sockaddr_in etc. */
#include <sys/un.h>             /* For struct sockaddr_un */
#include <arpa/inet.h>          /* For socklen_t */
#endif /* CYGWIN */

#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <signal.h>
#include <errno.h>
#include <stdlib.h>
#include <glib.h>
#include "configfile.h"         /* For UpdateConfigFile */
#include "dopewars.h"
#include "log.h"
#include "message.h"
#include "network.h"
#include "nls.h"
#include "serverside.h"
#include "tstring.h"
#include "util.h"

#ifdef GUI_SERVER
#include "gtkport/gtkport.h"
#endif

static const price_t MINTRENCHPRICE = 200, MAXTRENCHPRICE = 300;

#define ESCAPE      0
#define DEFECT      1
#define SHOT        2
#define NUMDISCOVER 3
char *Discover[NUMDISCOVER] = {
  /* Things that can "happen" to your spies - look for strings containing
     "The spy %s!" to see how these strings are used. */
  N_("escaped"), N_("defected"), N_("was shot")
};

/* The two keys that are valid answers to the Attack/Evade question. If
   you wish to translate them, do so in the same order as they given here.
   You will also need to translate the answers given by the clients. */
static char *attackquestiontr = N_("AE");

/* If we haven't talked to the metaserver for 3 hours, then remind it that
 * we still exist, so we don't get wiped from the list of active servers */
#define METAUPDATETIME  (10800)

/* Don't report players logging in/out to the metaserver more frequently
 * than once every minute (so as not to overload the metaserver, or slow
 * down our own server). */
#define METAMINTIME (60)

int TerminateRequest, ReregisterRequest, RelogRequest;

int MetaUpdateTimeout;
long MetaMinTimeout;
gboolean WantQuit = FALSE;

#ifdef CYGWIN
static SERVICE_STATUS_HANDLE scHandle;
#endif

#ifdef GUI_SERVER
static gboolean glib_timeout(gpointer userp);
static gboolean glib_socket(GIOChannel *ch, GIOCondition condition,
                            gpointer data);
#endif


/* Do we want to update the player details on the metaserver when the
 * timeout expires? */
gboolean MetaPlayerPending = FALSE;

GSList *FirstServer = NULL;

#ifdef NETWORKING
/* Data waiting to be sent to/read from the metaserver */
static CurlConnection MetaConn;

static GScanner *Scanner;

#endif

/* Handle to the high score file */
static FILE *ScoreFP = NULL;

/* Pointer to the filename of a pid file (if non-NULL) */
char *PidFile = NULL;

static char HelpText[] = {
  /* Help on various general server commands */
  N_("dopewars server version %s commands and settings\n\n"
     "help                     Displays this help screen\n"
     "list                     Lists all players logged on\n"
     "push <player>            Politely asks the named player to leave\n"
     "kill <player>            Abruptly breaks the connection with the "
     "named player\n"
     "msg:<mesg>               Send message to all players\n"
     "save <file>              Save current configuration to the named file\n"
     "quit                     Gracefully quit, after notifying all players\n"
     "<variable>=<value>       Sets the named variable to the given value\n"
     "<variable>               Displays the value of the named variable\n"
     "<list>[x].<var>=<value>  Sets the named variable in the given list,\n"
     "                         index x, to the given value\n"
     "<list>[x].<var>          Displays the value of the named list variable\n"
     "\nValid variables are listed below:-\n\n")
};

typedef enum _OfferForce {
  NOFORCE, FORCECOPS, FORCEBITCH
} OfferForce;

int SendSingleHighScore(Player *Play, struct HISCORE *Score,
                        int ind, gboolean Bold);
static int SendCopOffer(Player *To, OfferForce Force);
static int OfferObject(Player *To, gboolean ForceBitch);
static gboolean HighScoreWrite(FILE *fp, struct HISCORE *MultiScore,
                               struct HISCORE *AntiqueScore);

#ifdef NETWORKING
static void MetaConnectError(CurlConnection *conn, GError *err)
{
  dopelog(1, LF_SERVER, _("Failed to connect to metaserver at %s (%s)"),
          MetaServer.URL, err->message);
}

void log_meta_headers(gpointer data, gpointer user_data)
{
  char *header = data;
  if (*header)
    dopelog(4, LF_SERVER, _("MetaServer: %s"), header);
}
#endif

/* 
 * Sends server details to the metaserver, if specified. If "Up" is
 * TRUE, informs the metaserver that the server is now accepting
 * connections - otherwise tells the metaserver that this server is
 * about to go down. If "SendData" is TRUE, then also sends game
 * data (e.g. scores) to the metaserver. If "RespectTimeout" is TRUE
 * then the update is delayed if a previous update happened too
 * recently. If networking is disabled, this function does nothing.
 */
void RegisterWithMetaServer(gboolean Up, gboolean SendData,
                            gboolean RespectTimeout)
{
#ifdef NETWORKING
  struct HISCORE MultiScore[NUMHISCORE], AntiqueScore[NUMHISCORE];
  GString *body;
  gchar *prstr;
  gboolean ret;
  GError *tmp_error = NULL;
  int i;

  if (!MetaServer.Active || WantQuit || !Server) {
    return;
  }

  if (MetaMinTimeout > time(NULL) && RespectTimeout) {
    dopelog(3, LF_SERVER,
            _("Attempt to connect to metaserver too frequently "
              "- waiting for next timeout"));
    MetaPlayerPending = TRUE;
    return;
  }

  body = g_string_new("");

  g_string_assign(body, "output=text&");

  g_string_append_printf(body, "up=%d&port=%d&version=", Up ? 1 : 0, Port);
  AddURLEnc(body, VERSION);
  g_string_append_printf(body, "&players=%d&maxplay=%d&comment=",
                    CountPlayers(FirstServer), MaxClients);
  AddURLEnc(body, MetaServer.Comment);

  if (MetaServer.LocalName[0]) {
    g_string_append(body, "&hostname=");
    AddURLEnc(body, MetaServer.LocalName);
  }
  if (MetaServer.Password[0]) {
    g_string_append(body, "&password=");
    AddURLEnc(body, MetaServer.Password);
  }

  if (SendData && HighScoreRead(ScoreFP, MultiScore, AntiqueScore, TRUE)) {
    for (i = 0; i < NUMHISCORE; i++) {
      if (MultiScore[i].Name && MultiScore[i].Name[0]) {
        g_string_append_printf(body, "&nm[%d]=", i);
        AddURLEnc(body, MultiScore[i].Name);
        g_string_append_printf(body, "&dt[%d]=", i);
        AddURLEnc(body, MultiScore[i].Time);
        g_string_append_printf(body, "&st[%d]=%s&sc[%d]=", i,
                          MultiScore[i].Dead ? "dead" : "alive", i);
        AddURLEnc(body, prstr = FormatPrice(MultiScore[i].Money));
        g_free(prstr);
      }
    }
  }

  ret = OpenCurlConnection(&MetaConn, MetaServer.URL, body->str, &tmp_error);

  dopelog(2, LF_SERVER, _("Waiting for connect to metaserver at %s..."),
          MetaServer.URL);
  g_string_free(body, TRUE);
  if (!ret) {
    MetaConnectError(&MetaConn, tmp_error);
    g_error_free(tmp_error);
  }
  MetaPlayerPending = FALSE;

  MetaUpdateTimeout = time(NULL) + METAUPDATETIME;
  MetaMinTimeout = time(NULL) + METAMINTIME;
#endif /* NETWORKING */
}

#ifdef NETWORKING
void HandleServerPlayer(Player *Play)
{
  gchar *buf;
  gboolean MessageRead = FALSE;

  while ((buf = GetWaitingPlayerMessage(Play)) != NULL) {
    MessageRead = TRUE;
    HandleServerMessage(buf, Play);
    g_free(buf);
  }
  /* Reset the idle timeout (if necessary) */
  if (MessageRead && IdleTimeout) {
    Play->IdleTimeout = time(NULL) + (time_t) IdleTimeout;
  }
}
#endif /* NETWORKING */

/* 
 * Sends details (name, ID) about player "Play" to player "To", using
 * message code "Code".
 */
void SendPlayerDetails(Player *Play, Player *To, MsgCode Code)
{
  GString *text;

  text = g_string_new(GetPlayerName(Play));
  if (HaveAbility(To, A_PLAYERID)) {
    g_string_append_printf(text, "^%d", Play->ID);
  }
  SendServerMessage(NULL, C_NONE, Code, To, text->str);
  g_string_free(text, TRUE);
}

/*
 * Checks the version of the client that has connected, and sends a
 * warning message if it's old.
 */
void RemoteVersionCheck(Player *Play)
{
  /* Client didn't send a C_ABILITIES message at all, so is either broken
   * or is version 1.4.8 or earlier. */
  if (Play->Abil.RemoteNum == 0) {
    SendPrintMessage(NULL, C_VERSIONCHECK, Play,
        _("You appear to be using an extremely old (version 1.4.x) client.^"
          "While this will probably work, many of the newer features^"
          "will be unsupported. Get the latest version from the^"
          "dopewars website, https://dopewars.sourceforge.io/."));

  /* The client has a smaller value of A_NUM; this means that not only does
   * it not support some features, it doesn't even know they might exist. */
  } else if (Play->Abil.RemoteNum < A_NUM) {
    SendPrintMessage(NULL, C_VERSIONCHECK, Play,
        _("Warning: your client is too old to support all of this^"
          "server's features. For the full \"experience\", get^"
          "the latest version of dopewars from the^"
          "website, https://dopewars.sourceforge.io/."));
  }

  /* Otherwise, the client is either the same version as the server, or
   * it's newer. Both should be OK, so do nothing. */
}

/* 
 * Given a message "buf", from player "Play", performs processing and
 * sends suitable replies.
 */
void HandleServerMessage(gchar *buf, Player *Play)
{
  Player *To, *tmp, *pt;
  GSList *list;
  char *Data;
  AICode AI;
  MsgCode Code;
  gchar *text;
  DopeEntry NewEntry;
  int i;
  price_t money;

  if (ProcessMessage(buf, Play, &To, &AI, &Code, &Data, FirstServer) == -1) {
    g_warning("Bad message");
    return;
  }
  switch (Code) {
  case C_MSGTO:
    if (Network) {
      dopelog(3, LF_SERVER, "%s->%s: %s", GetPlayerName(Play),
              GetPlayerName(To), Data);
    }
    SendServerMessage(Play, AI, Code, To, Data);
    break;
  case C_ABILITIES:
    ReceiveAbilities(Play, Data);
    break;
  case C_NAME:
    StripTerminators(Data);
    pt = GetPlayerByName(Data, FirstServer);
    if (pt && pt != Play) {
      if (ConnectTimeout) {
        Play->ConnectTimeout = time(NULL) + (time_t) ConnectTimeout;
      }
      SendServerMessage(NULL, C_NONE, C_NEWNAME, Play, NULL);
    } else if (strlen(GetPlayerName(Play)) == 0 && Data[0]) {
      if (CountPlayers(FirstServer) < MaxClients || !Network) {
        RemoteVersionCheck(Play);
        SendAbilities(Play);
        CombineAbilities(Play);
        SendInitialData(Play);
        SendMiscData(Play);
        SetPlayerName(Play, Data);
        for (list = FirstServer; list; list = g_slist_next(list)) {
          pt = (Player *)list->data;
          if (pt != Play && IsConnectedPlayer(pt) && !IsCop(pt)) {
            SendPlayerDetails(pt, Play, C_LIST);
          }
        }
        if (ServerMOTD && ServerMOTD[0]) {
          SendPrintMessage(NULL, C_MOTD, Play, ServerMOTD);
        }
        SendServerMessage(NULL, C_NONE, C_ENDLIST, Play, NULL);
        RegisterWithMetaServer(TRUE, FALSE, TRUE);
        Play->ConnectTimeout = 0;

        if (Network) {
          dopelog(2, LF_SERVER, _("%s joins the game!"), GetPlayerName(Play));
        }
        for (list = FirstServer; list; list = g_slist_next(list)) {
          pt = (Player *)list->data;
          if (IsConnectedPlayer(pt) && pt != Play) {
            SendPlayerDetails(Play, pt, C_JOIN);
          }
        }
        Play->EventNum = E_ARRIVE;
        SendPlayerData(Play);
        SendEvent(Play);
      } else {
        /* Message displayed in the server when too many players try to
         * connect */
        dopelog(2, LF_SERVER,
                _("MaxClients (%d) exceeded - dropping connection"),
                MaxClients);
        if (MaxClients == 1) {
          text = g_strdup_printf(
                                  /* Message sent to a player if the
                                     server is full */
                                  _("Sorry, but this server has a limit of "
                                   "1 player, which has been reached.^"
                                   "Please try connecting again later."));
        } else {
          text = g_strdup_printf(
                                  /* Message sent to a player if the
                                     server is full */
                                  _("Sorry, but this server has a limit of "
                                   "%d players, which has been reached.^"
                                   "Please try connecting again later."),
                                  MaxClients);
        }
        SendServerMessage(NULL, C_NONE, C_PRINTMESSAGE, Play, text);
        g_free(text);
        /* Make sure they do actually disconnect, eventually! */
        if (ConnectTimeout) {
          Play->ConnectTimeout = time(NULL) + (time_t) ConnectTimeout;
        }
      }
    } else {
      /* A player changed their name during the game (unusual, and not
         really properly supported anyway) - notify all players of the
         change */
      dopelog(2, LF_SERVER, _("%s will now be known as %s"),
              GetPlayerName(Play), Data);
      BroadcastToClients(C_NONE, C_RENAME, Data, Play, Play);
      SetPlayerName(Play, Data);
    }
    break;
  case C_WANTQUIT:
    if (Play->EventNum != E_FINISH) {
      FinishGame(Play, NULL);
    }
    break;
  case C_REQUESTJET:
    i = atoi(Data);
    /* Make sure value is within range */
    if (i < 0 || i >= NumLocation) {
      dopelog(3, LF_SERVER, _("%s: DENIED jet to invalid location %s"),
              GetPlayerName(Play), Data);
      break;
    }
    if (Play->EventNum == E_FIGHT || Play->EventNum == E_FIGHTASK) {
      if (CanRunHere(Play)) {
        break;
      } else {
        RunFromCombat(Play, i);
      }
      if (Play->EventNum == E_WAITDONE) {
        Play->EventNum = Play->ResyncNum;
        SendEvent(Play);
      }
    }
    if (NumTurns > 0 && Play->Turn >= NumTurns
        && Play->EventNum != E_FINISH) {
      /* Message displayed when a player reaches their maximum number of
         turns */
      FinishGame(Play, _("Your dealing time is up..."));
    } else if (i != Play->IsAt && (NumTurns == 0 || Play->Turn < NumTurns)
               && Play->EventNum == E_NONE && Play->Health > 0) {
      dopelog(4, LF_SERVER, "%s jets to %s",
              GetPlayerName(Play), Location[i].Name);
      Play->IsAt = i;
      Play->Turn++;
      g_date_add_days(Play->date, 1);
      Play->Debt = Play->Debt * (DebtInterest + 100) / 100;
      Play->Debt = MAX(Play->Debt, 0);
      Play->Bank = Play->Bank * (BankInterest + 100) / 100;
      Play->Bank = MAX(Play->Bank, 0);
      SendPlayerData(Play);
      Play->EventNum = E_SUBWAY;
      SendEvent(Play);
    } else {
      /* A player has tried to jet to a new location, but we don't allow
         them to. (e.g. they're still fighting someone, or they're
         supposed to be dead) */
      dopelog(3, LF_SERVER, _("%s: DENIED jet to %s"),
              GetPlayerName(Play), Location[i].Name);
    }
    break;
  case C_REQUESTSCORE:
    SendHighScores(Play, FALSE, NULL);
    break;
  case C_CONTACTSPY:
    for (list = FirstServer; list; list = g_slist_next(list)) {
      tmp = (Player *)list->data;
      i = GetListEntry(&(tmp->SpyList), Play);
      if (tmp != Play && i >= 0 && tmp->SpyList.Data[i].Turns >= 0) {
        SendSpyReport(Play, tmp);
      }
    }
    break;
  case C_DEPOSIT:
    money = strtoprice(Data);
    if (Play->EventNum == E_BANK && Play->Bank + money >= 0
        && Play->Cash - money >= 0) {
      Play->Bank += money;
      Play->Cash -= money;
      SendPlayerData(Play);
    }
    break;
  case C_PAYLOAN:
    money = strtoprice(Data);
    if (Play->EventNum == E_LOANSHARK && money > 0
        && Play->Debt - money >= 0 && Play->Cash - money >= 0) {
      Play->Debt -= money;
      Play->Cash -= money;
      SendPlayerData(Play);
    }
    break;
  case C_BUYOBJECT:
    BuyObject(Play, Data);
    break;
  case C_FIGHTACT:
    if (Data[0] == 'R')
      RunFromCombat(Play, -1);
    else
      Fire(Play);
    break;
  case C_ANSWER:
    HandleAnswer(Play, To, Data);
    break;
  case C_DONE:
    if (Play->EventNum == E_WAITDONE) {
      Play->EventNum = Play->ResyncNum;
      SendEvent(Play);
    } else if (Play->EventNum != E_NONE && Play->EventNum < E_OUTOFSYNC) {
      Play->EventNum++;
      SendEvent(Play);
    }
    break;
  case C_SPYON:
    if (Play->Cash >= Prices.Spy) {
      dopelog(3, LF_SERVER, _("%s now spying on %s"), GetPlayerName(Play),
              GetPlayerName(To));
      Play->Cash -= Prices.Spy;
      LoseBitch(Play, NULL, NULL);
      NewEntry.Play = Play;
      NewEntry.Turns = -1;
      AddListEntry(&(To->SpyList), &NewEntry);
      SendPlayerData(Play);
    } else {
      dopelog(2, LF_SERVER, _("%s spy on %s: DENIED"), GetPlayerName(Play),
                GetPlayerName(To));
    }
    break;
  case C_TIPOFF:
    if (Play->Cash >= Prices.Tipoff) {
      dopelog(3, LF_SERVER, _("%s tipped off the cops to %s"),
              GetPlayerName(Play), GetPlayerName(To));
      Play->Cash -= Prices.Tipoff;
      LoseBitch(Play, NULL, NULL);
      NewEntry.Play = Play;
      NewEntry.Turns = 0;
      AddListEntry(&(To->TipList), &NewEntry);
      SendPlayerData(Play);
    } else {
      g_warning(_("%s tipoff about %s: DENIED"), GetPlayerName(Play),
                GetPlayerName(To));
    }
    break;
  case C_SACKBITCH:
    if (Play->Bitches.Carried > 0) {
      LoseBitch(Play, NULL, NULL);
      SendPlayerData(Play);
    }
    break;
  case C_MSG:
    if (Network)
      dopelog(3, LF_SERVER, "%s: %s", GetPlayerName(Play), Data);
    BroadcastToClients(C_NONE, C_MSG, Data, Play, Play);
    break;
  default:
    dopelog(0, LF_SERVER, _("Unknown message: %s:%c:%s:%s"),
            GetPlayerName(Play), Code, GetPlayerName(To), Data);
    break;
  }
}

/* 
 * Notifies all clients that player "Play" has left the game and
 * cleans up after them if necessary.
 */
void ClientLeftServer(Player *Play)
{
  Player *tmp;
  GSList *list;

  if (!IsConnectedPlayer(Play))
    return;

  if (Play->EventNum == E_FIGHT || Play->EventNum == E_FIGHTASK) {
    WithdrawFromCombat(Play);
  }
  for (list = FirstServer; list; list = g_slist_next(list)) {
    tmp = (Player *)list->data;
    if (tmp != Play) {
      RemoveAllEntries(&(tmp->TipList), Play);
      RemoveAllEntries(&(tmp->SpyList), Play);
    }
  }
  BroadcastToClients(C_NONE, C_LEAVE, GetPlayerName(Play), Play, Play);
}

/* 
 * Closes down the server and frees up associated handles and memory.
 */
void CleanUpServer()
{
  while (FirstServer) {
    FirstServer = RemovePlayer((Player *)FirstServer->data, FirstServer);
  }
#ifdef NETWORKING
  if (Server)
    CloseSocket(ListenSock);
#endif
}

/* 
 * Responds to a SIGUSR1 signal, and requests the main event loop to
 * reregister the server with the dopewars metaserver.
 */
void ReregisterHandle(int sig)
{
  ReregisterRequest = 1;
}

/* 
 * Responds to a SIGHUP signal, and requests the main event loop to
 * close and then reopen the log file (if any).
 */
void RelogHandle(int sig)
{
  RelogRequest = 1;
}

/* 
 * Traps an attempt by the user to send dopewars a SIGTERM or SIGINT
 * (e.g. pressing Ctrl-C) and signals for a "nice" shutdown. Restores
 * the default signal action (to terminate without cleanup) so that
 * the user can still close the program easily if this cleanup code
 * then causes problems or long delays.
 */
void BreakHandle(int sig)
{
  struct sigaction sact;

  TerminateRequest = 1;
  sact.sa_handler = SIG_DFL;
  sact.sa_flags = 0;
  sigemptyset(&sact.sa_mask);
  sigaction(SIGTERM, &sact, NULL);
  sigaction(SIGINT, &sact, NULL);
}

/* 
 * Prints the server help screen to the given file pointer.
 */
void PrintHelpTo(FILE *fp)
{
  int i;
  GString *VarName;

  VarName = g_string_new("");
  fprintf(fp, _(HelpText), VERSION);
  for (i = 0; i < NUMGLOB; i++) {
    if (Globals[i].NameStruct[0]) {
      g_string_printf(VarName, "%s%s.%s", Globals[i].NameStruct,
                       Globals[i].StructListPt ? "[x]" : "",
                       Globals[i].Name);
    } else {
      g_string_assign(VarName, Globals[i].Name);
    }
    fprintf(fp, "%-26s %s\n", VarName->str, _(Globals[i].Help));
  }
  fputs("\n\n", fp);
  g_string_free(VarName, TRUE);
}

/* 
 * Displays a simple help screen listing the server commands and options.
 */
void ServerHelp(void)
{
  int i;
  GString *VarName;

  VarName = g_string_new("");
  g_print(_(HelpText), VERSION);
  for (i = 0; i < NUMGLOB; i++) {
    if (Globals[i].NameStruct[0]) {
      g_string_printf(VarName, "%s%s.%s", Globals[i].NameStruct,
                       Globals[i].StructListPt ? "[x]" : "",
                       Globals[i].Name);
    } else {
      g_string_assign(VarName, Globals[i].Name);
    }
    g_print("%-26s\t%s\n", VarName->str, _(Globals[i].Help));
  }
  g_string_free(VarName, TRUE);
}

#ifdef NETWORKING
static NetworkBuffer *reply_netbuf;
static void ServerReply(const gchar *msg)
{
  int msglen;
  gchar *msgcp;

  if (reply_netbuf) {
    msglen = strlen(msg);
    while (msglen > 0 && msg[msglen - 1] == '\n')
      msglen--;
    if (msglen > 0) {
      msgcp = g_strndup(msg, msglen);
      QueueMessageForSend(reply_netbuf, msgcp);
      g_free(msgcp);
    }
  } else {
    g_print("%s", msg);
  }
}

/* 
 * Creates a pid file (if "PidFile" is non-NULL) and writes the process
 * ID into it.
 */
void CreatePidFile(void)
{
  FILE *fp;

  if (!PidFile)
    return;
  fp = fopen(PidFile, "w");
  if (fp) {
    dopelog(1, LF_SERVER, _("Maintaining pid file %s"), PidFile);
    fprintf(fp, "%ld\n", (long)getpid());
    fclose(fp);
    chmod(PidFile, S_IREAD | S_IWRITE);
  } else {
    gchar *OpenError = ErrStrFromErrno(errno);
    g_warning(_("Cannot create pid file %s: %s"), PidFile, OpenError);
    g_free(OpenError);
  }
}

/* 
 * Removes the previously-created pid file "PidFile".
 */
void RemovePidFile(void)
{
  if (PidFile)
    unlink(PidFile);
}

static gboolean StartServer(void)
{
  LastError *sockerr = NULL;
  GString *errstr;

#ifndef CYGWIN
  struct sigaction sact;
#endif

  if (!CheckHighScoreFileConfig())
    return FALSE;
  Scanner = g_scanner_new(&ScannerConfig);
  Scanner->msg_handler = ScannerErrorHandler;
  Scanner->input_name = "(stdin)";

  /* Make the output line-buffered, so that the log file (if used) is
   * updated regularly */
  fflush(stdout);

#ifdef SETVBUF_REVERSED         /* 2nd and 3rd arguments are reversed on
                                 * some systems */
  setvbuf(stdout, _IOLBF, (char *)NULL, 0);
#else
  setvbuf(stdout, (char *)NULL, _IOLBF, 0);
#endif

  Network = Server = TRUE;
  FirstServer = NULL;
  ClientMessageHandlerPt = NULL;
  ListenSock = CreateTCPSocket(&sockerr);
  if (ListenSock == SOCKET_ERROR) {
    errstr = g_string_new("");
    g_string_assign_error(errstr, sockerr);
    g_log(NULL, G_LOG_LEVEL_CRITICAL,
          _("Cannot create server (listening) socket (%s) Aborting."),
          errstr->str);
    g_string_free(errstr, TRUE);
    FreeError(sockerr);
    exit(EXIT_FAILURE);
  }

  /* This doesn't seem to work properly under Win32 */
#ifndef CYGWIN
  SetReuse(ListenSock);
#endif

  SetBlocking(ListenSock, FALSE);

  if (!BindTCPSocket(ListenSock, BindAddress, Port, &sockerr)) {
    errstr = g_string_new("");
    g_string_assign_error(errstr, sockerr);
    g_log(NULL, G_LOG_LEVEL_CRITICAL,
          _("Cannot bind to port %u (%s) Aborting."), Port, errstr->str);
    g_string_free(errstr, TRUE);
    FreeError(sockerr);
    exit(EXIT_FAILURE);
  }

  if (listen(ListenSock, 10) == SOCKET_ERROR) {
    g_log(NULL, G_LOG_LEVEL_CRITICAL,
          _("Cannot listen to network socket. Aborting."));
    exit(EXIT_FAILURE);
  }

  /* Initial startup message for the server */
  dopelog(0, LF_SERVER, 
          _("dopewars server version %s ready and waiting for "
            "connections on port %d."), VERSION, Port);

  MetaUpdateTimeout = MetaMinTimeout = 0;

  TerminateRequest = ReregisterRequest = RelogRequest = 0;

#if !CYGWIN
  sact.sa_handler = ReregisterHandle;
  sact.sa_flags = 0;
  sigemptyset(&sact.sa_mask);
  if (sigaction(SIGUSR1, &sact, NULL) == -1) {
    /* Warning messages displayed if we fail to trap various signals */
    g_warning(_("Cannot install SIGUSR1 interrupt handler!"));
  }
  sact.sa_handler = RelogHandle;
  sact.sa_flags = 0;
  sigemptyset(&sact.sa_mask);
  if (sigaction(SIGHUP, &sact, NULL) == -1) {
    g_warning(_("Cannot install SIGHUP interrupt handler!"));
  }
  sact.sa_handler = BreakHandle;
  sact.sa_flags = 0;
  sigemptyset(&sact.sa_mask);
  if (sigaction(SIGINT, &sact, NULL) == -1) {
    g_warning(_("Cannot install SIGINT interrupt handler!"));
  }
  if (sigaction(SIGTERM, &sact, NULL) == -1) {
    g_warning(_("Cannot install SIGTERM interrupt handler!"));
  }
  sact.sa_handler = SIG_IGN;
  sact.sa_flags = 0;
  if (sigaction(SIGPIPE, &sact, NULL) == -1) {
    g_warning(_("Cannot install pipe handler!"));
  }
#endif
  return TRUE;
}

static void InitMetaServer()
{
  CurlInit(&MetaConn);
#ifdef GUI_SERVER
  SetCurlCallback(&MetaConn, glib_timeout, glib_socket);
#endif
  RegisterWithMetaServer(TRUE, TRUE, FALSE);
}

/* 
 * Begin the process of shutting down the server. In order to do this,
 * we need to log out all of the currently connected players, and tell
 * the metaserver that we're shutting down. We only shut down properly
 * once all of these messages have been completely sent and
 * acknowledged. (Of course, this can be overridden by a SIGINT or
 * similar in the case of unresponsive players.)
 */
void RequestServerShutdown(void)
{
  RegisterWithMetaServer(FALSE, FALSE, FALSE);
  BroadcastToClients(C_NONE, C_QUIT, NULL, NULL, NULL);
  WantQuit = TRUE;
}

/* 
 * Returns TRUE if the actions initiated by RequestServerShutdown()
 * have been successfully completed, such that we can shut down the
 * server properly now.
 */
gboolean IsServerShutdown(void)
{
  return (WantQuit && !FirstServer && !MetaConn.running);
}

static GPrintFunc StartServerReply(NetworkBuffer *netbuf)
{
  reply_netbuf = netbuf;
  if (netbuf)
    return g_set_print_handler(ServerReply);
  else
    return NULL;
}

static void FinishServerReply(GPrintFunc oldprint)
{
  if (oldprint)
    g_set_print_handler(oldprint);
}

static void ServerSaveConfigFile(const char *string)
{
  gchar *file = NULL;

  if (!string) {
    file = GetLocalConfigFile();
    string = file;
  }
  if (UpdateConfigFile(file, FALSE)) {
    g_print(_("Configuration file saved OK as %s\n"), string);
  }
  g_free(file);
}

static void HandleServerCommand(char *string, NetworkBuffer *netbuf,
                                gboolean ForceUTF8)
{
  GSList *list;
  Player *tmp;
  GPrintFunc oldprint;
  Converter *conv;

  oldprint = StartServerReply(netbuf);

  conv = Conv_New();
  if (ForceUTF8) {
    Conv_SetCodeset(conv, "UTF-8");
  }
  g_scanner_input_text(Scanner, string, strlen(string));
  if (!ParseNextConfig(Scanner, conv, NULL, TRUE)) {
    if (g_ascii_strncasecmp(string, "help", 4) == 0 || g_ascii_strncasecmp(string, "h", 1) == 0
        || strcmp(string, "?") == 0) {
      ServerHelp();
    } else if (g_ascii_strncasecmp(string, "quit", 4) == 0) {
      RequestServerShutdown();
    } else if (g_ascii_strncasecmp(string, "msg:", 4) == 0) {
      BroadcastToClients(C_NONE, C_MSG, string + 4, NULL, NULL);
    } else if (g_ascii_strncasecmp(string, "save ", 5) == 0) {
      ServerSaveConfigFile(string + 5);
    } else if (g_ascii_strncasecmp(string, "save", 4) == 0) {
      ServerSaveConfigFile(NULL);
    } else if (g_ascii_strncasecmp(string, "list", 4) == 0) {
      if (FirstServer) {
        g_print(_("Users currently logged on:-\n"));
        for (list = FirstServer; list; list = g_slist_next(list)) {
          tmp = (Player *)list->data;
          if (!IsCop(tmp)) {
            g_print("%s\n", GetPlayerName(tmp));
          }
        }
      } else
        g_print(_("No users currently logged on!\n"));
    } else if (g_ascii_strncasecmp(string, "push ", 5) == 0) {
      tmp = GetPlayerByName(string + 5, FirstServer);
      if (tmp) {
        g_print(_("Pushing %s\n"), GetPlayerName(tmp));
        SendServerMessage(NULL, C_NONE, C_PUSH, tmp, NULL);
      } else
        g_print(_("No such user!\n"));
    } else if (g_ascii_strncasecmp(string, "kill ", 5) == 0) {
      tmp = GetPlayerByName(string + 5, FirstServer);
      if (tmp) {
        /* The named user has been removed from the server following
           a "kill" command */
        g_print(_("%s killed\n"), GetPlayerName(tmp));
        BroadcastToClients(C_NONE, C_KILL, GetPlayerName(tmp), tmp,
                           (Player *)FirstServer->data);
        FirstServer = RemovePlayer(tmp, FirstServer);
      } else
        g_print(_("No such user!\n"));
    } else {
      g_print(_("Unknown command - try \"help\" for help...\n"));
    }
  }
  Conv_Free(conv);
  FinishServerReply(oldprint);
}

Player *HandleNewConnection(void)
{
  socklen_t cadsize;
  int ClientSock;
  struct sockaddr_in ClientAddr;
  Player *tmp;
  cadsize = sizeof(struct sockaddr);
  if ((ClientSock = accept(ListenSock, (struct sockaddr *)&ClientAddr,
                            &cadsize)) == -1) {
    perror("accept socket");
    exit(EXIT_FAILURE);
  }
  dopelog(2, LF_SERVER, _("got connection from %s"),
          inet_ntoa(ClientAddr.sin_addr));
  tmp = g_new(Player, 1);

  FirstServer = AddPlayer(ClientSock, tmp, FirstServer);
  if (ConnectTimeout) {
    tmp->ConnectTimeout = time(NULL) + (time_t) ConnectTimeout;
  }
  return tmp;
}

void StopServer()
{
  dopelog(0, LF_SERVER, _("dopewars server terminating."));
  g_scanner_destroy(Scanner);
  CleanUpServer();
  RemovePidFile();
}

void RemovePlayerFromServer(Player *Play)
{
  if (!WantQuit && strlen(GetPlayerName(Play)) > 0) {
    dopelog(2, LF_SERVER, _("%s leaves the server!"), GetPlayerName(Play));
    ClientLeftServer(Play);
    /* Blank the name, so that CountPlayers ignores this player */
    SetPlayerName(Play, NULL);
    /* Report the new high scores (if any) and the new number of players
     * to the metaserver */
    RegisterWithMetaServer(TRUE, TRUE, TRUE);
  }
  FirstServer = RemovePlayer(Play, FirstServer);
}

#ifndef CYGWIN
static gchar sockpref[] = "/tmp/.dopewars";

static gchar *GetLocalSockDir(void)
{
  return g_strdup_printf("%s-%u", sockpref, Port);
}

gchar *GetLocalSocket(void)
{
  return g_strdup_printf("%s-%u/socket", sockpref, Port);
}

static void CloseLocalSocket(int localsock)
{
  gchar *sockname, *sockdir;

  if (localsock >= 0)
    close(localsock);

  sockname = GetLocalSocket();
  sockdir = GetLocalSockDir();
  unlink(sockname);
  rmdir(sockdir);
  g_free(sockname);
  g_free(sockdir);
}

static int SetupLocalSocket(void)
{
  int sock;
  struct sockaddr_un addr;
  gchar *sockname, *sockdir;

  CloseLocalSocket(-1);

  sock = socket(PF_UNIX, SOCK_STREAM, 0);
  if (sock == -1)
    return -1;

  SetBlocking(sock, FALSE);

  sockname = GetLocalSocket();
  sockdir = GetLocalSockDir();
  if (mkdir(sockdir, S_IRUSR | S_IWUSR | S_IXUSR) == -1)
    return -1;

  addr.sun_family = AF_UNIX;
  strncpy(addr.sun_path, sockname, sizeof(addr.sun_path));
  addr.sun_path[sizeof(addr.sun_path) - 1] = '\0';

  if (bind(sock, (struct sockaddr *)&addr, sizeof(struct sockaddr_un)) == -1)
    return -1;

  chmod(sockname, S_IRUSR | S_IWUSR);
  g_free(sockname);
  g_free(sockdir);

  listen(sock, 10);

  return sock;
}
#endif

static void LogMetaReply(CurlConnection *conn)
{
  g_ptr_array_foreach(conn->headers, log_meta_headers, NULL);
  char *ch = conn->data;
  while(ch && *ch) {
    char *nextch = CurlNextLine(conn, ch);
    if (*ch)
      dopelog(2, LF_SERVER, _("MetaServer: %s"), ch);
    ch = nextch;
  }
  dopelog(4, LF_SERVER, _("MetaServer: (closed)"));
}

#ifdef GUI_SERVER
static gboolean glib_timeout(gpointer userp)
{
  CurlConnection *g = userp;
  int still_running;
  GError *err = NULL;
  if (!CurlConnectionSocketAction(g, CURL_SOCKET_TIMEOUT, 0, &still_running,
                                  &err)) {
    MetaConnectError(g, err);
    g_error_free(err);
  }
  g->timer_event = 0;
  return G_SOURCE_REMOVE;
}

static void GuiQuitServer()
{
  gtk_main_quit();
  StopServer();
}

/* Called by glib when we get action on a multi socket */
static gboolean glib_socket(GIOChannel *ch, GIOCondition condition,
                            gpointer data)
{
  CurlConnection *g = (CurlConnection*) data;
  int still_running;
  GError *err = NULL;
  int fd = g_io_channel_unix_get_fd(ch);
  int action =
    ((condition & G_IO_IN) ? CURL_CSELECT_IN : 0) |
    ((condition & G_IO_OUT) ? CURL_CSELECT_OUT : 0);

  if (!CurlConnectionSocketAction(g, fd, action, &still_running, &err)) {
    MetaConnectError(g, err);
    g_error_free(err);
  }
  if (still_running) {
    return TRUE;
  } else {
    if (g->timer_event) {
      dp_g_source_remove(g->timer_event);
      g->timer_event = 0;
    }
    LogMetaReply(g);
    CloseCurlConnection(g);
    if (IsServerShutdown())
      GuiQuitServer();
    return FALSE;
  }
}

#endif

/* 
 * Initializes server, processes network and interactive messages, and
 * finally cleans up the server on exit.
 */
void ServerLoop(struct CMDLINE *cmdline)
{
  Player *tmp;
  GSList *list, *listcp;
  fd_set readfs, writefs, errorfs;
  int topsock;
  struct timeval timeout;
  int MinTimeout;
  GString *LineBuf;

  gboolean DoneOK;

#ifndef CYGWIN
  gchar *buf;
  int localsock;
  GSList *nextlist;
  GPrintFunc oldprint;
  GSList *localconn = NULL;
#endif

  InitConfiguration(cmdline);

  if (!StartServer())
    return;

#ifdef HAVE_FORK
  /* Daemonize; continue if the fork was successful and we are the child,
   * or if the fork failed */
  if (Daemonize && fork() > 0)
    return;
#endif
  CreatePidFile();
  InitMetaServer();

#ifndef CYGWIN
  localsock = SetupLocalSocket();
  if (localsock == -1) {
    dopelog(0, LF_SERVER,
            _("Could not set up Unix domain socket for admin "
              "connections - check permissions on /tmp!"));
  }
#endif

  LineBuf = g_string_new("");
  while (1) {
    FD_ZERO(&readfs);
    FD_ZERO(&writefs);
    FD_ZERO(&errorfs);
    curl_multi_fdset(MetaConn.multi, &readfs, &writefs, &errorfs, &topsock);
    FD_SET(ListenSock, &readfs);
    FD_SET(ListenSock, &errorfs);
    topsock = MAX(topsock + 1, ListenSock + 1);
#ifndef CYGWIN
    if (localsock >= 0) {
      FD_SET(localsock, &readfs);
      topsock = MAX(topsock, localsock + 1);
    }
    for (list = localconn; list; list = g_slist_next(list)) {
      NetworkBuffer *netbuf;

      netbuf = (NetworkBuffer *)list->data;
      SetSelectForNetworkBuffer(netbuf, &readfs, &writefs, &errorfs,
                                &topsock);
    }
#endif
    for (list = FirstServer; list; list = g_slist_next(list)) {
      tmp = (Player *)list->data;
      if (!IsCop(tmp)) {
        SetSelectForNetworkBuffer(&tmp->NetBuf, &readfs, &writefs,
                                  &errorfs, &topsock);
      }
    }
    MinTimeout = GetMinimumTimeout(FirstServer);
    if (MinTimeout != -1) {
      timeout.tv_sec = MinTimeout;
      timeout.tv_usec = 0;
    }
    if (select(topsock, &readfs, &writefs, &errorfs,
               MinTimeout == -1 ? NULL : &timeout) == -1) {
      if (errno == EINTR) {
        if (ReregisterRequest) {
          ReregisterRequest = 0;
          RegisterWithMetaServer(TRUE, TRUE, FALSE);
          continue;
        } else if (TerminateRequest) {
          TerminateRequest = 0;
          RequestServerShutdown();
          if (IsServerShutdown())
            break;
          else
            continue;
        } else if (RelogRequest) {      /* Re-open log file */
          RelogRequest = 0;
          CloseLog();
          OpenLog();
          continue;
        } else
          continue;
      }
      perror("select");
      break;
    }
    FirstServer = HandleTimeouts(FirstServer);
    if (FD_ISSET(ListenSock, &readfs)) {
      HandleNewConnection();
    }
#ifndef CYGWIN
    if (localsock >= 0 && FD_ISSET(localsock, &readfs)) {
      int newlocal;
      NetworkBuffer *netbuf;

      newlocal = accept(localsock, NULL, NULL);
      netbuf = g_new(NetworkBuffer, 1);

      InitNetworkBuffer(netbuf, '\n', '\r', NULL);
      BindNetworkBufferToSocket(netbuf, newlocal);
      localconn = g_slist_append(localconn, netbuf);
      oldprint = StartServerReply(netbuf);
      g_print(_("dopewars server version %s ready for admin commands; "
                "try \"help\" for help"), VERSION);
      FinishServerReply(oldprint);
      dopelog(1, LF_SERVER, _("New admin connection"));
    }
    list = localconn;
    while (list) {
      NetworkBuffer *netbuf;

      nextlist = g_slist_next(list);
      netbuf = (NetworkBuffer *)list->data;
      if (netbuf) {
        if (RespondToSelect(netbuf, &readfs, &writefs, &errorfs, &DoneOK)) {
          while ((buf = GetWaitingMessage(netbuf)) != NULL) {
            dopelog(2, LF_SERVER, _("Admin command: %s"), buf);
            HandleServerCommand(buf, netbuf, FALSE);
            g_free(buf);
          }
        }
        if (!DoneOK) {
          dopelog(1, LF_SERVER, _("Admin connection closed"));
          localconn = g_slist_remove(localconn, netbuf);
          ShutdownNetworkBuffer(netbuf);
          g_free(netbuf);
        }
        list = nextlist;
      }
    }
    if (IsServerShutdown())
      break;
#endif
    if (MetaConn.running) {
      GError *tmp_error = NULL;
      int still_running;
      if (!CurlConnectionPerform(&MetaConn, &still_running, &tmp_error)) {
        MetaConnectError(&MetaConn, tmp_error);
	g_error_free(tmp_error);
        if (IsServerShutdown())
          break;
      } else if (still_running == 0) {
        LogMetaReply(&MetaConn);
        CloseCurlConnection(&MetaConn);
        if (IsServerShutdown())
          break;
      }
    }

    /* Check all players for data; iterate over a copy of the player list,
     * as HandleServerPlayer may remove players from this list! */
    listcp = g_slist_copy(FirstServer);
    for (list = listcp; list; list = g_slist_next(list)) {
      if (list->data && g_slist_find(FirstServer, list->data)) {
        tmp = (Player *)list->data;
        if (RespondToSelect(&tmp->NetBuf, &readfs, &writefs,
                            &errorfs, &DoneOK)) {
          /* If any complete messages were read, process them */
          HandleServerPlayer(tmp);
        }
        if (!DoneOK) {
          /* The socket has been shut down, or the buffer was filled -
           * remove player */
          RemovePlayerFromServer(tmp);
          if (IsServerShutdown()) {
            break;
          }
        }
      }
    }
    g_slist_free(listcp);
    if (IsServerShutdown()) {
      break;
    }
  }
#ifndef CYGWIN
  CloseLocalSocket(localsock);
#endif
  StopServer();
  g_string_free(LineBuf, TRUE);

  CurlCleanup(&MetaConn);
}

#ifdef GUI_SERVER
static GtkWidget *TextOutput;
static gint ListenTag = 0;
static void SocketStatus(NetworkBuffer *NetBuf, gboolean Read,
                         gboolean Write, gboolean Exception, gboolean CallNow);
static void GuiSetTimeouts(void);
static time_t NextTimeout = 0;
static guint TimeoutTag = 0;

static gboolean GuiDoTimeouts(gpointer data)
{
  /* Forget the TimeoutTag so that GuiSetTimeouts doesn't delete it -
   * it'll be deleted automatically anyway when we return FALSE */
  TimeoutTag = 0;
  NextTimeout = 0;

  FirstServer = HandleTimeouts(FirstServer);
  GuiSetTimeouts();
  return FALSE;
}

void GuiSetTimeouts(void)
{
  int MinTimeout;
  time_t TimeNow;

  TimeNow = time(NULL);
  MinTimeout = GetMinimumTimeout(FirstServer);
  if (TimeNow + MinTimeout < NextTimeout || NextTimeout < TimeNow) {
    if (TimeoutTag > 0)
      dp_g_source_remove(TimeoutTag);
    TimeoutTag = 0;
    if (MinTimeout > 0) {
      TimeoutTag = dp_g_timeout_add(MinTimeout * 1000, GuiDoTimeouts, NULL);
      NextTimeout = TimeNow + MinTimeout;
    }
  }
}

static void GuiServerPrintFunc(const gchar *string)
{
  TextViewAppend(GTK_TEXT_VIEW(TextOutput), string, NULL, TRUE);
}

static void GuiServerLogMessage(const gchar *log_domain,
                                GLogLevelFlags log_level,
                                const gchar *message, gpointer user_data)
{
  GString *text;

  text = GetLogString(log_level, message);
  if (text) {
    g_string_append(text, "\n");
    GuiServerPrintFunc(text->str);
    g_string_free(text, TRUE);
  }
}

#ifdef CYGWIN
static void ServiceFailure(const gchar *log_domain,
                           GLogLevelFlags log_level, const gchar *message,
                           gpointer user_data)
{
  SERVICE_STATUS status;

  g_print("%s\n", message);
  status.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
  status.dwCurrentState = SERVICE_STOPPED;
  status.dwControlsAccepted = SERVICE_ACCEPT_STOP;
  status.dwWin32ExitCode = ERROR_NETWORK_UNREACHABLE;
  status.dwCheckPoint = 0;
  status.dwWaitHint = 0;
  SetServiceStatus(scHandle, &status);
}
#endif

static void GuiDoCommand(GtkWidget *widget, gpointer data)
{
  gchar *text;

  text = gtk_editable_get_chars(GTK_EDITABLE(widget), 0, -1);
  gtk_editable_delete_text(GTK_EDITABLE(widget), 0, -1);
  HandleServerCommand(text, NULL, TRUE);
  g_free(text);
  if (IsServerShutdown())
    GuiQuitServer();
}

static gboolean GuiHandleSocket(GIOChannel *source, GIOCondition condition,
                                gpointer data)
{
  Player *Play;
  gboolean DoneOK;

  Play = (Player *)data;

  /* Sanity check - is the player still around? */
  if (!g_slist_find(FirstServer, (gpointer)Play))
    return TRUE;

  if (PlayerHandleNetwork(Play, condition & G_IO_IN, condition & G_IO_OUT,
                          condition & G_IO_ERR, &DoneOK)) {
    HandleServerPlayer(Play);
    GuiSetTimeouts();           /* We may have set some new timeouts */
  }
  if (!DoneOK) {
    RemovePlayerFromServer(Play);
    if (IsServerShutdown())
      GuiQuitServer();
  }
  return TRUE;
}

void SocketStatus(NetworkBuffer *NetBuf, gboolean Read, gboolean Write,
                  gboolean Exception, gboolean CallNow)
{
  if (NetBuf->InputTag)
    dp_g_source_remove(NetBuf->InputTag);
  NetBuf->InputTag = 0;
  if (Read || Write) {
    NetBuf->InputTag = dp_g_io_add_watch(NetBuf->ioch,
                                     (Read ? G_IO_IN : 0) |
                                     (Write ? G_IO_OUT : 0) |
                                     (Exception ? G_IO_ERR : 0),
                                     GuiHandleSocket,
                                     NetBuf->CallBackData);
  }
  if (CallNow)
    GuiHandleSocket(NetBuf->ioch, 0, NetBuf->CallBackData);
}

static gboolean GuiNewConnect(GIOChannel *source, GIOCondition condition,
                              gpointer data)
{
  Player *Play;

  if (condition & G_IO_IN) {
    Play = HandleNewConnection();
    SetNetworkBufferCallBack(&Play->NetBuf, SocketStatus, (gpointer)Play);
  }
  return TRUE;
}

static gboolean TriedPoliteShutdown = FALSE;

static gint GuiRequestDelete(GtkWidget *widget, GdkEvent * event,
                             gpointer data)
{
  if (TriedPoliteShutdown) {
    GuiQuitServer();
  } else {
    TriedPoliteShutdown = TRUE;
    HandleServerCommand("quit", NULL, FALSE);
    if (IsServerShutdown())
      GuiQuitServer();
  }
  return TRUE;                  /* Never allow automatic deletion - we
                                 * handle it manually */
}

#ifdef CYGWIN
static HWND mainhwnd = NULL;
static BOOL systray = FALSE;

static BOOL RegisterStatus(DWORD state)
{
  SERVICE_STATUS status;

  status.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
  status.dwCurrentState = state;
  status.dwControlsAccepted = SERVICE_ACCEPT_STOP;
  status.dwWin32ExitCode = NO_ERROR;
  status.dwCheckPoint = 0;
  status.dwWaitHint = 5000;
  return SetServiceStatus(scHandle, &status);
}

static VOID WINAPI ServiceHandler(DWORD control)
{
  DWORD state = SERVICE_RUNNING;

  switch (control) {
  case SERVICE_CONTROL_STOP:
    state = SERVICE_STOP_PENDING;
    break;
  }
  if (!RegisterStatus(state)) {
    dopelog(0, LF_SERVER, _("Failed to set NT Service status"));
    return;
  }

  if (mainhwnd
      && !PostMessage(mainhwnd, MYWM_SERVICE, 0, (LPARAM) control)) {
    dopelog(0, LF_SERVER, _("Failed to post service notification message"));
    return;
  }
}

static VOID WINAPI ServiceInit(DWORD argc, LPTSTR * argv)
{
  scHandle = RegisterServiceCtrlHandler("dopewars-server", ServiceHandler);
  if (!scHandle) {
    dopelog(0, LF_SERVER, _("Failed to register service handler"));
    return;
  }
  if (!RegisterStatus(SERVICE_START_PENDING)) {
    dopelog(0, LF_SERVER, _("Failed to set NT Service status"));
    return;
  }

  GuiServerLoop(NULL, TRUE);

  if (!RegisterStatus(SERVICE_STOPPED)) {
    dopelog(0, LF_SERVER, _("Failed to set NT Service status"));
    return;
  }
}

void ServiceMain(struct CMDLINE *cmdline)
{
  SERVICE_TABLE_ENTRY services[] = {
    {"dopewars-server", ServiceInit},
    {NULL, NULL}
  };

  InitConfiguration(cmdline);

  if (!StartServiceCtrlDispatcher(services)) {
    dopelog(0, LF_SERVER, _("Failed to start NT Service"));
  }
}

static LRESULT CALLBACK GuiServerWndProc(HWND hwnd, UINT msg,
                                         WPARAM wparam, LPARAM lparam)
{
  if (hwnd == mainhwnd)
    switch (msg) {
    case MYWM_SERVICE:
      if (lparam == SERVICE_CONTROL_STOP) {
        RequestServerShutdown();
      }
      break;
    case MYWM_TASKBAR:
      if ((UINT) lparam == WM_LBUTTONDOWN)
        ShowWindow(mainhwnd, SW_SHOW);
      break;
    case WM_SYSCOMMAND:
      if (wparam == SC_MINIMIZE && systray) {
        ShowWindow(mainhwnd, SW_HIDE);
        return TRUE;
      }
      break;
    }
  return FALSE;
}

static void SetupTaskBarIcon(GtkWidget *widget)
{
  NOTIFYICONDATA nid;

  nid.cbSize = sizeof(NOTIFYICONDATA);
  nid.uID = 1000;
  if (widget && !widget->hWnd)
    return;
  if (!widget && !mainhwnd)
    return;

  nid.hWnd = mainhwnd;
  if (widget && MinToSysTray) {
    nid.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP;
    nid.uCallbackMessage = MYWM_TASKBAR;
    nid.hIcon = mainIcon;
    strcpy(nid.szTip, "dopewars server - running");
    systray = Shell_NotifyIcon(NIM_ADD, &nid);
  } else {
    systray = FALSE;
    Shell_NotifyIcon(NIM_DELETE, &nid);
  }
}
#endif /* CYGWIN */

void GuiServerLoop(struct CMDLINE *cmdline, gboolean is_service)
{
  GtkWidget *window, *text, *hbox, *vbox, *entry, *label;
  GIOChannel *listench;

  /* GTK+2 (and the GTK emulation code on WinNT systems) expects all
   * strings to be UTF-8, so we force gettext to return all translations
   * in this encoding here. */
  bind_textdomain_codeset(PACKAGE, "UTF-8");

  Conv_SetInternalCodeset("UTF-8");
  WantUTF8Errors(TRUE);

  if (cmdline) {
    InitConfiguration(cmdline);
  }

  window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
  g_signal_connect(G_OBJECT(window), "delete_event",
                   G_CALLBACK(GuiRequestDelete), NULL);
  gtk_window_set_default_size(GTK_WINDOW(window), 500, 250);

  /* Title of dopewars server window (if used) */
  gtk_window_set_title(GTK_WINDOW(window), _("dopewars server"));

  gtk_container_set_border_width(GTK_CONTAINER(window), 7);

  vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 7);
  TextOutput = text = gtk_scrolled_text_view_new(&hbox);
  gtk_text_view_set_editable(GTK_TEXT_VIEW(text), FALSE);
  gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(text), GTK_WRAP_WORD);
  gtk_box_pack_start(GTK_BOX(vbox), hbox, TRUE, TRUE, 0);

  hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
  label = gtk_label_new(_("Command:"));
  gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 0);
  entry = gtk_entry_new();
  g_signal_connect(G_OBJECT(entry), "activate",
                   G_CALLBACK(GuiDoCommand), NULL);
  gtk_box_pack_start(GTK_BOX(hbox), entry, TRUE, TRUE, 0);
  gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0);

  gtk_container_add(GTK_CONTAINER(window), vbox);
  gtk_widget_show_all(window);

  if (is_service) {
#ifdef CYGWIN
    g_log_set_handler(NULL, G_LOG_LEVEL_CRITICAL, ServiceFailure, NULL);
#endif
  } else {
    g_set_print_handler(GuiServerPrintFunc);
    g_log_set_handler(NULL,
                      LogMask() | G_LOG_LEVEL_MESSAGE |
                      G_LOG_LEVEL_WARNING, GuiServerLogMessage, NULL);
  }
  if (!StartServer())
    return;
  InitMetaServer();

#ifdef CYGIN
  listench = g_io_channel_win32_new_socket(ListenSock);
#else
  listench = g_io_channel_unix_new(ListenSock);
#endif
  ListenTag = dp_g_io_add_watch(listench, G_IO_IN, GuiNewConnect, NULL);
#ifdef CYGWIN
  mainhwnd = window->hWnd;
  SetupTaskBarIcon(window);
  SetCustomWndProc(GuiServerWndProc);
  if (is_service && !RegisterStatus(SERVICE_RUNNING)) {
    dopelog(0, LF_SERVER, _("Failed to set NT Service status"));
    return;
  }
#endif
  gtk_main();
#ifdef CYGWIN
  SetupTaskBarIcon(NULL);
#endif
}
#endif /* GUI_SERVER */

#endif /* NETWORKING */

/* 
 * Tells player "Play" that the game is over; display "Message".
 */
void FinishGame(Player *Play, char *Message)
{
  Play->EventNum = E_FINISH;
  ClientLeftServer(Play);
  SendHighScores(Play, TRUE, Message);

  /* Blank the name, so that CountPlayers ignores this player */
  SetPlayerName(Play, NULL);

  /* Make sure they do actually disconnect, eventually! */
  if (ConnectTimeout) {
    Play->ConnectTimeout = time(NULL) + (time_t) ConnectTimeout;
  }
}

/* 
 * Reads a batch of NUMHISCORE high scores into "HiScore" from "fp".
 */
void HighScoreTypeRead(struct HISCORE *HiScore, FILE *fp)
{
  int i;
  char *buf;

  for (i = 0; i < NUMHISCORE; i++) {
    if (read_string(fp, &HiScore[i].Name) == EOF)
      break;
    read_string(fp, &HiScore[i].Time);
    read_string(fp, &buf);
    HiScore[i].Money = strtoprice(buf);
    g_free(buf);
    HiScore[i].Dead = (fgetc(fp) > 0);
  }
}

/* 
 * Writes out a batch of NUMHISCORE high scores from "HiScore" to "fp".
 */
void HighScoreTypeWrite(struct HISCORE *HiScore, FILE *fp)
{
  int i;
  gchar *text;

  for (i = 0; i < NUMHISCORE; i++) {
    if (HiScore[i].Name) {
      fwrite(HiScore[i].Name, strlen(HiScore[i].Name) + 1, 1, fp);
    } else
      fputc(0, fp);
    if (HiScore[i].Time) {
      fwrite(HiScore[i].Time, strlen(HiScore[i].Time) + 1, 1, fp);
    } else
      fputc(0, fp);
    text = pricetostr(HiScore[i].Money);
    fwrite(text, strlen(text) + 1, 1, fp);
    g_free(text);
    fputc(HiScore[i].Dead ? 1 : 0, fp);
  }
}

/* 
 * Closes the high score file opened by OpenHighScoreFile, below.
 */
void CloseHighScoreFile()
{
  if (ScoreFP) {
    fclose(ScoreFP);
  }
  ScoreFP = NULL;
}

/* 
 * If we're running setuid/setgid, drop down to the privilege level of the
 * user that started the dopewars process.
 */
void DropPrivileges()
{
#ifndef CYGWIN

#ifdef HAVE_ISSETUGID
  if (issetugid() == 0) return;
#endif

  /* Ignore the return from setregid; we'll check it ourselves to be sure
   * (this avoids problems when running under fakeroot) */
  setregid(getgid(), getgid());
  if (getgid() != getegid()) {
    perror("setregid");
    exit(EXIT_FAILURE);
  }

  setreuid(getuid(), getuid());
  if (getuid() != geteuid()) {
    perror("setreuid");
    exit(EXIT_FAILURE);
  }
#endif
}

static const gchar SCOREHEADER[] = "DOPEWARS SCORES V.";
static const guint SCOREHDRLEN = sizeof(SCOREHEADER) - 1; /* Don't include \0 */
static const guint SCOREVERSION = 1;

static gboolean HighScoreReadHeader(FILE *fp, gint *ScoreVersion)
{
  gchar *header;

  if (read_string(fp, &header) != EOF) {
    if (header && strlen(header) > SCOREHDRLEN &&
        strncmp(header, SCOREHEADER, SCOREHDRLEN) == 0) {
      if (ScoreVersion)
        *ScoreVersion = atoi(header + SCOREHDRLEN);
      g_free(header);
      return TRUE;
    }
  }
  g_free(header);
  return FALSE;
}

static void HighScoreWriteHeader(FILE *fp)
{
  gchar *header;

  header = g_strdup_printf("%s%d", SCOREHEADER, SCOREVERSION);
  fwrite(header, strlen(header) + 1, 1, fp);
  g_free(header);
}

/* 
 * Converts an old format high score file to the new format.
 */
void ConvertHighScoreFile(const gchar *convertfile)
{
  FILE *old;
  gchar *BackupFile;
  int ch;
  struct HISCORE MultiScore[NUMHISCORE], AntiqueScore[NUMHISCORE];

  BackupFile = g_strdup_printf("%s.bak", convertfile);

  old = fopen(convertfile, "r+");
  if (old) {
    gboolean empty;
    /* Only convert if the file is not empty, and does not have a header */
    rewind(old);
    empty = (fgetc(old) == EOF);
    rewind(old);
    if (!empty && !HighScoreReadHeader(old, NULL)) {
      FILE *backup = fopen(BackupFile, "w");
      if (backup) {
        /* Make a backup of the old file */
        ftruncate(fileno(backup), 0);
        rewind(backup);
        rewind(old);
        while (1) {
          ch = fgetc(old);
          if (ch == EOF) {
            break;
	  } else {
            fputc(ch, backup);
          }
        }
        fclose(backup);

        /* Read in the scores without the header, and then write out with
         * the header */
        if (!HighScoreRead(old, MultiScore, AntiqueScore, FALSE)) {
          g_log(NULL, G_LOG_LEVEL_CRITICAL,
                _("Error reading scores from %s."), convertfile);
        } else {
          ftruncate(fileno(old), 0);
          rewind(old);
          if (HighScoreWrite(old, MultiScore, AntiqueScore)) {
            g_message(_("The high score file %s has been converted to the "
                        "new format.\nA backup of the old file has been "
                        "created as %s.\n"), convertfile, BackupFile);
          }
        }
      } else {
        gchar *errmsg = ErrStrFromErrno(errno);
        g_log(NULL, G_LOG_LEVEL_CRITICAL,
              _("Cannot create backup (%s) of the\nhigh score file: %s."),
              BackupFile, errmsg);
        g_free(errmsg);
      }
    }
    fclose(old);
  } else {
    gchar *errmsg = ErrStrFromErrno(errno);
    g_log(NULL, G_LOG_LEVEL_CRITICAL,
          _("Cannot open high score file %s: %s."),
          convertfile, errmsg);
    g_free(errmsg);
  }

  g_free(BackupFile);
}

#ifdef CYGWIN
/* Try to open a high score file in the Win32-specific AppData directory */
static FILE *OpenHighScoreAppData(int *error, gboolean *empty)
{
  FILE *fp = NULL;
  HKEY key;
  static const char *subkey = "Software\\Microsoft\\Windows\\CurrentVersion"
                              "\\Explorer\\Shell Folders";
  static const char *subval = "Local AppData";

  *empty = FALSE;

  if (RegOpenKeyEx(HKEY_CURRENT_USER, subkey, 0, KEY_READ,
                   &key) == ERROR_SUCCESS) {
    DWORD keylen, keytype;
    if (RegQueryValueEx(key, subval, NULL, &keytype, NULL,
                        &keylen) == ERROR_SUCCESS && keytype == REG_SZ) {
      char *keyval = g_malloc(keylen);
      if (RegQueryValueEx(key, subval, NULL, &keytype, (LPBYTE)keyval,
                          &keylen) == ERROR_SUCCESS) {
        GString *str = g_string_sized_new(keylen + 40);
        g_string_assign(str, keyval);
        g_free(keyval);
        g_string_append(str, "\\dopewars");
        CreateDirectory(str->str, NULL);
        g_string_append(str, "\\dopewars.sco");
        fp = fopen(str->str, "r+");
        if (!fp) {
          fp = fopen(str->str, "w+");
          if (!fp) {
            *error = errno;
          }
          *empty = TRUE;
        }
        g_string_free(str, 1);
      }
    }
    RegCloseKey(key);
  }
  return fp;
}
#endif


/* State, set by OpenHighScoreFile, and later used by
 * CheckHighScoreFileConfig */
static gboolean EmptyFile;
static int OpenError;

/* 
 * Opens the high score file for later use, and then drops privileges.
 */
void OpenHighScoreFile(void)
{
  if (ScoreFP) {
    return;                     /* If already opened, then we're done */
  }

  EmptyFile = FALSE;
  OpenError = 0;

  /* Win32 gets upset if we use "a+" so we use this nasty hack instead */
  ScoreFP = fopen(HiScoreFile, "r+");
  if (!ScoreFP) {
    ScoreFP = fopen(HiScoreFile, "w+");
    if (!ScoreFP) {
      OpenError = errno;
    }
    EmptyFile = TRUE;
  }
#ifdef CYGWIN
  if (!ScoreFP) {
    ScoreFP = OpenHighScoreAppData(&OpenError, &EmptyFile);
  }
#endif

  /* Check for a 0-byte score file */
  if (ScoreFP && !EmptyFile) {
    rewind(ScoreFP);
    if (fgetc(ScoreFP) == EOF) {
      EmptyFile = TRUE;
    }
    rewind(ScoreFP);
  }
}

/* 
 * Checks the high score file opened by OpenHighScoreFile, above. Also warns
 * the user about other problems encountered during startup. Returns
 * TRUE if it's valid; otherwise, returns FALSE.
 */
gboolean CheckHighScoreFileConfig(void)
{

  if (!ScoreFP) {
    gchar *errstr = ErrStrFromErrno(OpenError);
    g_log(NULL, G_LOG_LEVEL_CRITICAL,
          _("Cannot open high score file %s.\n"
            "(%s.) Either ensure you have permissions to access\n"
            "this file and directory, or specify an alternate high score "
            "file with the\n-f command line option."),
          HiScoreFile, errstr);
    g_free(errstr);
    return FALSE;
  }

  if (EmptyFile) {
    HighScoreWriteHeader(ScoreFP);
    fflush(ScoreFP);
  } else if (!HighScoreReadHeader(ScoreFP, NULL)) {
    g_log(NULL, G_LOG_LEVEL_CRITICAL,
          _("%s does not appear to be a valid\n"
            "high score file - please check it. If it is a high score file\n"
            "from an older version of dopewars, then first convert it to the\n"
            "new format by running \"dopewars -C %s\"\n"
            "from the command line."), HiScoreFile, HiScoreFile);
    return FALSE;
  }

  if (ConfigErrors) {
#ifdef CYGWIN
    g_warning(_("Errors were encountered during the reading of the "
                "configuration file.\nAs as result, some settings may not "
                "work as expected. Please consult the\n"
                "file \"dopewars-log.txt\" for further details."));
#else
    g_warning(_("Errors were encountered during the reading of the "
                "configuration\nfile. As a result, some settings may not "
                "work as expected. Please see the\nmessages on standard "
                "output for further details."));
#endif
  }

  return TRUE;
}

/* 
 * Reads all the high scores into MultiScore and AntiqueScore (antique
 * mode scores). If ReadHeader is TRUE, read the high score file header
 * first. Returns TRUE on success, FALSE on failure.
 */
gboolean HighScoreRead(FILE *fp, struct HISCORE *MultiScore,
                       struct HISCORE *AntiqueScore, gboolean ReadHeader)
{
  gint ScoreVersion = 0;
  memset(MultiScore, 0, sizeof(struct HISCORE) * NUMHISCORE);
  memset(AntiqueScore, 0, sizeof(struct HISCORE) * NUMHISCORE);
  if (fp && ReadLock(fp) == 0) {
    rewind(fp);
    if (ReadHeader && !HighScoreReadHeader(fp, &ScoreVersion)) {
      ReleaseLock(fp);
      return FALSE;
    }
    HighScoreTypeRead(AntiqueScore, fp);
    HighScoreTypeRead(MultiScore, fp);
    ReleaseLock(fp);
  } else
    return FALSE;
  return TRUE;
}

/* 
 * Writes out all the high scores from MultiScore and AntiqueScore; returns
 * TRUE on success, FALSE on failure.
 */
gboolean HighScoreWrite(FILE *fp, struct HISCORE *MultiScore,
                        struct HISCORE *AntiqueScore)
{
  if (fp && WriteLock(fp) == 0) {
    ftruncate(fileno(fp), 0);
    rewind(fp);
    HighScoreWriteHeader(fp);
    HighScoreTypeWrite(AntiqueScore, fp);
    HighScoreTypeWrite(MultiScore, fp);
    ReleaseLock(fp);
    fflush(fp);
  } else
    return 0;
  return 1;
}

/* 
 * Adds "Play" to the high score list if necessary, and then sends the
 * scores over the network to "Play".
 * If "EndGame" is TRUE, add the current score if it's high enough and
 * display an explanatory message. "Message" is tacked onto the start
 * if it's non-NULL. The client is then informed that the game's over.
 */
void SendHighScores(Player *Play, gboolean EndGame, char *Message)
{
  struct HISCORE MultiScore[NUMHISCORE], AntiqueScore[NUMHISCORE], Score;
  struct HISCORE *HiScore;
  struct tm *timep;
#ifdef HAVE_GMTIME_R
  struct tm tmbuf;
#endif

  time_t tim;
  GString *text;
  int i, j, InList = -1;

  text = g_string_new("");
  if (!HighScoreRead(ScoreFP, MultiScore, AntiqueScore, TRUE)) {
    g_warning(_("Unable to read high score file %s"), HiScoreFile);
  }
  if (Message) {
    g_string_assign(text, Message);
    if (strlen(text->str) > 0)
      g_string_append_c(text, '^');
  }
  if (WantAntique)
    HiScore = AntiqueScore;
  else
    HiScore = MultiScore;
  if (EndGame) {
    Score.Money = Play->Cash + Play->Bank - Play->Debt;
    Score.Name = g_strdup(GetPlayerName(Play));
    Score.Dead = (Play->Health == 0);
    tim = time(NULL);
#ifdef HAVE_GMTIME_R
    timep = gmtime_r(&tim, &tmbuf);
#else
    timep = gmtime(&tim);
#endif
    Score.Time = g_new(char, 80);       /* Yuck! */

    strftime(Score.Time, 80, "%d-%m-%Y", timep);
    Score.Time[79] = '\0';
    for (i = 0; i < NUMHISCORE; i++) {
      if (InList == -1 && (Score.Money > HiScore[i].Money ||
                           !HiScore[i].Time || HiScore[i].Time[0] == 0)) {
        InList = i;
        g_string_append(text,
                        _("Congratulations! You made the high scores!"));
        SendPrintMessage(NULL, C_NONE, Play, text->str);
        g_free(HiScore[NUMHISCORE - 1].Name);
        g_free(HiScore[NUMHISCORE - 1].Time);
        for (j = NUMHISCORE - 1; j > i; j--) {
          memcpy(&HiScore[j], &HiScore[j - 1], sizeof(struct HISCORE));
        }
        memcpy(&HiScore[i], &Score, sizeof(struct HISCORE));
        break;
      }
    }
    if (InList == -1) {
      g_string_append(text,
                      _("You didn't even make the high score table..."));
      SendPrintMessage(NULL, C_NONE, Play, text->str);
    }
  }
  SendServerMessage(NULL, C_NONE, C_STARTHISCORE, Play, NULL);

  j = 0;
  for (i = 0; i < NUMHISCORE; i++) {
    if (SendSingleHighScore(Play, &HiScore[i], j, InList == i))
      j++;
  }
  if (InList == -1 && EndGame) {
    SendSingleHighScore(Play, &Score, j, TRUE);
    g_free(Score.Name);
    g_free(Score.Time);
  }
  SendServerMessage(NULL, C_NONE, C_ENDHISCORE, Play,
                    EndGame ? "end" : NULL);
  if (!EndGame)
    SendDrugsHere(Play, FALSE);
  if (EndGame && !HighScoreWrite(ScoreFP, MultiScore, AntiqueScore)) {
    g_warning(_("Unable to write high score file %s"), HiScoreFile);
  }
  for (i = 0; i < NUMHISCORE; i++) {
    g_free(MultiScore[i].Name);
    g_free(MultiScore[i].Time);
    g_free(AntiqueScore[i].Name);
    g_free(AntiqueScore[i].Time);
  }
  g_string_free(text, TRUE);
}

/* 
 * Sends a single high score in "Score" with position "ind" to player
 * "Play". If Bold is TRUE, instructs the client to display the score in
 * bold text.
 */
int SendSingleHighScore(Player *Play, struct HISCORE *Score,
                        int ind, gboolean Bold)
{
  gchar *Data, *prstr;

  if (!Score->Time || Score->Time[0] == 0)
    return 0;
  Data = g_strdup_printf("%d^%c%c%18s  %-14s %-34s %8s%c", ind,
                         Bold ? 'B' : 'N', Bold ? '>' : ' ',
                         prstr = FormatPrice(Score->Money),
                         Score->Time, Score->Name,
                         Score->Dead ? _("(R.I.P.)") : "",
                         Bold ? '<' : ' ');
  SendServerMessage(NULL, C_NONE, C_HISCORE, Play, Data);
  g_free(prstr);
  g_free(Data);
  return 1;
}

/* 
 * In order for the server to keep track of the state of each client, each
 * client's state is identified by its EventNum data member. So, for example,
 * there is a state for fighting the cops, a state for going to the bank, and
 * so on. This function instructs client player "To" to carry out the actions
 * expected of it in its current state. It is the client's responsibility to
 * ensure that it carries out the correct actions to advance itself to the
 * "next" state; if it fails in this duty it will hang!
 */
void SendEvent(Player *To)
{
  price_t Money;
  int i, j;
  gchar *text;
  Player *Play;
  GSList *list;

  if (!To)
    return;
  if (To->EventNum == E_MAX)
    To->EventNum = E_NONE;
  if (To->EventNum == E_NONE || To->EventNum >= E_OUTOFSYNC)
    return;
  Money = To->Cash + To->Bank - To->Debt;

  ClearPrices(To);

  while (To->EventNum < E_MAX) {
    switch (To->EventNum) {
    case E_SUBWAY:
      SendServerMessage(NULL, C_NONE, C_SUBWAYFLASH, To, NULL);
      break;
    case E_OFFOBJECT:
      To->OnBehalfOf = NULL;
      for (i = 0; i < To->TipList.Number; i++) {
        dopelog(3, LF_SERVER, _("%s: Tipoff from %s"), GetPlayerName(To),
                GetPlayerName(To->TipList.Data[i].Play));
        To->OnBehalfOf = To->TipList.Data[i].Play;
        SendCopOffer(To, FORCECOPS);
        return;
      }
      for (i = 0; i < To->SpyList.Number; i++) {
        if (To->SpyList.Data[i].Turns < 0) {
          dopelog(3, LF_SERVER, _("%s: Spy offered by %s"), GetPlayerName(To),
                  GetPlayerName(To->SpyList.Data[i].Play));
          To->OnBehalfOf = To->SpyList.Data[i].Play;
          SendCopOffer(To, FORCEBITCH);
          return;
        }
        To->SpyList.Data[i].Turns++;
        if (To->SpyList.Data[i].Turns > 3 &&
            brandom(0, 100) < 10 + To->SpyList.Data[i].Turns) {
          if (TotalGunsCarried(To) > 0)
            j = brandom(0, NUMDISCOVER);
          else
            j = brandom(0, NUMDISCOVER - 1);
          text =
              dpg_strdup_printf(_("One of your %tde was spying for %s."
                                  "^The spy %s!"), Names.Bitches,
                                GetPlayerName(To->SpyList.Data[i].Play),
                                _(Discover[j]));
          if (j != DEFECT)
            LoseBitch(To, NULL, NULL);
          SendPlayerData(To);
          SendPrintMessage(NULL, C_NONE, To, text);
          g_free(text);
          text = g_strdup_printf(_("Your spy working with %s has "
                                   "been discovered!^The spy %s!"),
                                 GetPlayerName(To), _(Discover[j]));
          if (j == ESCAPE)
            GainBitch(To->SpyList.Data[i].Play);
          To->SpyList.Data[i].Play->Flags &= ~SPYINGON;
          SendPlayerData(To->SpyList.Data[i].Play);
          SendPrintMessage(NULL, C_NONE, To->SpyList.Data[i].Play, text);
          g_free(text);
          RemoveListEntry(&(To->SpyList), i);
          i--;
        }
      }
      if (Money > 3000000)
        i = 130;
      else if (Money > 1000000)
        i = 115;
      else
        i = 100;
      if (brandom(0, i) > 75) {
        if (SendCopOffer(To, NOFORCE))
          return;
      }
      break;
    case E_SAYING:
      if (!Sanitized && brandom(0, 100) < 15
          && (NumSubway > 0 || NumPlaying > 0)) {
        int subwaychance = 50;

        if (NumSubway == 0)
          subwaychance = 0;
        if (NumPlaying == 0)
          subwaychance = 100;
        if (brandom(0, 100) < subwaychance) {
          text = g_strdup_printf(_("The lady next to you on the subway "
                                   "said,^ \"%s\"%s"),
                                 SubwaySaying[brandom(0, NumSubway)],
                                 brandom(0, 100) < 30 ?
                                 _("^    (at least, you -think- that's "
                                   "what she said)") : "");
        } else {
          text = g_strdup_printf(_("You hear someone playing %s"),
                                 Playing[brandom(0, NumPlaying)]);
        }
        SendPrintMessage(NULL, C_NONE, To, text);
        g_free(text);
      }
      break;
    case E_LOANSHARK:
      if (To->IsAt + 1 == LoanSharkLoc && To->Debt > 0) {
        text = dpg_strdup_printf(_("YN^Would you like to visit %tde?"),
                                 Names.LoanSharkName);
        SendQuestion(NULL, C_ASKLOAN, To, text);
        g_free(text);
        return;
      }
      break;
    case E_BANK:
      if (To->IsAt + 1 == BankLoc) {
        text = dpg_strdup_printf(_("YN^Would you like to visit %tde?"),
                                 Names.BankName);
        SendQuestion(NULL, C_ASKBANK, To, text);
        g_free(text);
        return;
      }
      break;
    case E_GUNSHOP:
      if (To->IsAt + 1 == GunShopLoc && !Sanitized && NumGun > 0) {
        text = dpg_strdup_printf(_("YN^Would you like to visit %tde?"),
                                 Names.GunShopName);
        SendQuestion(NULL, C_ASKGUNSHOP, To, text);
        g_free(text);
        return;
      }
      break;
    case E_ROUGHPUB:
      if (To->IsAt + 1 == RoughPubLoc) {
        text = dpg_strdup_printf(_("YN^Would you like to visit %tde?"),
                                 Names.RoughPubName);
        SendQuestion(NULL, C_ASKPUB, To, text);
        g_free(text);
        return;
      }
      break;
    case E_HIREBITCH:
      if (To->IsAt + 1 == RoughPubLoc) {
        To->Bitches.Price = prandom(Bitch.MinPrice, Bitch.MaxPrice);
        text =
            dpg_strdup_printf(_
                              ("YN^^Would you like to hire a %tde for %P?"),
                              Names.Bitch, To->Bitches.Price);
        SendQuestion(NULL, C_ASKBITCH, To, text);
        g_free(text);
        return;
      }
      break;
    case E_ARRIVE:
      for (list = FirstServer; list; list = g_slist_next(list)) {
        Play = (Player *)list->data;
        if (IsConnectedPlayer(Play) && Play != To
            && NumGun > 0 && Play->IsAt == To->IsAt
            && Play->EventNum == E_NONE && TotalGunsCarried(To) > 0) {
          text = g_strdup_printf(_("%s^%s is already here!^"
                                   "Do you Attack, or Evade?"),
                                 attackquestiontr,
                                 GetPlayerName(Play));
          /* Steal this to keep track of the potential defender */
          To->OnBehalfOf = Play;

          SendDrugsHere(To, TRUE);
          SendQuestion(NULL, C_MEETPLAYER, To, text);
          g_free(text);
          return;
        }
      }
      SendDrugsHere(To, TRUE);
      break;
    default:
      break;
    }
    To->EventNum++;
  }
  if (To->EventNum >= E_MAX)
    To->EventNum = E_NONE;
}

/* 
 * In response to client player "To" being in state E_OFFOBJECT,
 * randomly engages the client in combat with the cops or offers
 * other random events. Returns 0 if the client should then be
 * advanced to the next state, 1 otherwise (i.e. if there are
 * questions pending which the client must answer first)
 * If Force==FORCECOPS, engage in combat with the cops for certain
 * If Force==FORCEBITCH, offer the client a bitch for certain
 */
int SendCopOffer(Player *To, OfferForce Force)
{
  int i;

  /* The cops are more likely to attack in locations with higher police
   * presence ratings */
  i = brandom(0, 80 + Location[To->IsAt].PolicePresence);

  if (Force == FORCECOPS)
    i = 100;
  else if (Force == FORCEBITCH)
    i = 0;
  else
    To->OnBehalfOf = NULL;
  if (i < 33) {
    return (OfferObject(To, Force == FORCEBITCH));
  } else if (i < 50) {
    return (RandomOffer(To));
  } else if (Sanitized || NumCop == 0 || NumGun == 0) {
    return 0;
  } else {
    CopsAttackPlayer(To);
    return 1;
  }
  return 1;
}

/* 
 * Has the cops attack player "Play".
 */
void CopsAttackPlayer(Player *Play)
{
  Player *Cops;
  gint CopIndex, NumDeputy, GunIndex;

  if (NumCop == 0 || NumGun == 0) {
    g_warning(_("No cops or guns!"));
    return;
  }

  CopIndex = 1 - Play->CopIndex;
  if (CopIndex < 0) {
    g_warning(_("Cops cannot attack other cops!"));
    return;
  }
  if (CopIndex > NumCop)
    CopIndex = NumCop;
  Cops = g_new(Player, 1);

  FirstServer = AddPlayer(0, Cops, FirstServer);
  SetPlayerName(Cops, Cop[CopIndex - 1].Name);
  Cops->CopIndex = CopIndex;
  Cops->Cash = brandom(100, 2000);
  Cops->Debt = Cops->Bank = 0;

  NumDeputy = brandom(Cop[CopIndex - 1].MinDeputies,
                      Cop[CopIndex - 1].MaxDeputies);
  Cops->Bitches.Carried = NumDeputy;
  GunIndex = Cop[CopIndex - 1].GunIndex;
  if (GunIndex >= NumGun)
    GunIndex = NumGun - 1;
  Cops->Guns[GunIndex].Carried =
      (NumDeputy * Cop[CopIndex - 1].DeputyGun) + Cop[CopIndex - 1].CopGun;
  Cops->Health = 100;

  Play->EventNum++;
  AttackPlayer(Cops, Play);
}

/* 
 * Starts combat between player "Play" and player "Attacked"; if
 * either player is currently engaged in combat, add the other
 * player to the existing combat. If neither player is currently
 * fighting, start a new combat between them. Either player can be
 * the cops.
 */
void AttackPlayer(Player *Play, Player *Attacked)
{
  GPtrArray *FightArray;

  g_assert(Play && Attacked);

  if (Play->FightArray && Attacked->FightArray) {
    if (Play->FightArray == Attacked->FightArray) {
      g_error(_("Players are already in a fight!"));
    } else {
      g_error(_("Players are already in separate fights!"));
    }
    return;
  }
  if (NumGun == 0) {
    g_error(_("Cannot start fight - no guns to use!"));
    return;
  }

  if (!Play->FightArray && !Attacked->FightArray) {
    FightArray = g_ptr_array_new();
  } else {
    FightArray =
        Play->FightArray ? Play->FightArray : Attacked->FightArray;
  }

  if (!Play->FightArray) {
    Play->ResyncNum = Play->EventNum;
    g_ptr_array_add(FightArray, Play);
  }
  if (!Attacked->FightArray) {
    Attacked->ResyncNum = Attacked->EventNum;
    g_ptr_array_add(FightArray, Attacked);
  }
  Play->FightArray = Attacked->FightArray = FightArray;
  Play->EventNum = Attacked->EventNum = E_FIGHT;

  Play->Attacking = Attacked;

  SendFightMessage(Attacked, Play, 0, F_ARRIVED, (price_t)0, TRUE, NULL);

  Fire(Play);
}

/* 
 * Returns TRUE if player "Other" is not allied with player "Play".
 */
gboolean IsOpponent(Player *Play, Player *Other)
{
  return TRUE;
}

void HandleDamage(Player *Defend, Player *Attack, int Damage,
                  int *BitchesKilled, price_t *Loot)
{
  Inventory *Guns, *Drugs;
  price_t Bounty;

  Guns = (Inventory *)g_malloc0(sizeof(Inventory) * NumGun);
  Drugs = (Inventory *)g_malloc0(sizeof(Inventory) * NumDrug);
  ClearInventory(Guns, Drugs);

  Bounty = 0;
  if (Defend->Health <= Damage && Defend->Bitches.Carried == 0) {
    Bounty = Defend->Cash + Defend->Bank - Defend->Debt;
    AddInventory(Guns, Defend->Guns, NumGun);
    AddInventory(Drugs, Defend->Drugs, NumDrug);
    Defend->Health = 0;
  } else if (Defend->Bitches.Carried > 0 && Defend->Health <= Damage) {
    if (IsCop(Defend))
      LoseBitch(Defend, NULL, NULL);
    else
      LoseBitch(Defend, Guns, Drugs);
    Defend->Health = 100;
    *BitchesKilled = 1;
  } else {
    Defend->Health -= Damage;
  }
  if (IsCop(Attack)) {          /* Don't let cops loot players */
    ClearInventory(Guns, Drugs);
  } else {
    TruncateInventoryFor(Guns, Drugs, Attack);
  }
  SendPlayerData(Defend);
  if (Bounty < 0)
    Bounty = 0;
  if (!IsInventoryClear(Guns, Drugs)) {
    AddInventory(Attack->Guns, Guns, NumGun);
    AddInventory(Attack->Drugs, Drugs, NumDrug);
    ChangeSpaceForInventory(Guns, Drugs, Attack);
  }
  Attack->Cash += Bounty;
  if (Bounty > 0 || !IsInventoryClear(Guns, Drugs)) {
    if (Bounty > 0)
      *Loot = Bounty;
    else
      *Loot = -1;
    SendPlayerData(Attack);
  }
  g_free(Guns);
  g_free(Drugs);
}

void GetFightRatings(Player *Attack, Player *Defend,
                     int *AttackRating, int *DefendRating)
{
  int i;

  /* Base values */
  *AttackRating = 80;
  *DefendRating = 100;

  for (i = 0; i < NumGun; i++) {
    *AttackRating += Gun[i].Damage * Attack->Guns[i].Carried;
  }
  if (IsCop(Attack))
    *AttackRating -= Cop[Attack->CopIndex - 1].AttackPenalty;

  *DefendRating -= 5 * Defend->Bitches.Carried;
  if (IsCop(Defend))
    *DefendRating -= Cop[Defend->CopIndex - 1].DefendPenalty;

  *DefendRating = MAX(*DefendRating, 10);
  *AttackRating = MAX(*AttackRating, 10);
}

void AllowNextShooter(Player *Play)
{
  Player *NextShooter;

  if (FightTimeout) {
    NextShooter = GetNextShooter(Play);
    if (NextShooter && !CanPlayerFire(NextShooter)) {
      NextShooter->FightTimeout = 0;
    }
  }
}

void DoReturnFire(Player *Play)
{
  guint ArrayInd;
  Player *Defend;

  if (!Play || !Play->FightArray)
    return;

  if (FightTimeout != 0 || !IsCop(Play)) {
    for (ArrayInd = 0;
         Play->FightArray && ArrayInd < Play->FightArray->len;
         ArrayInd++) {
      Defend = (Player *)g_ptr_array_index(Play->FightArray, ArrayInd);
      if (IsCop(Defend) && CanPlayerFire(Defend))
        Fire(Defend);
    }
  }
}

/*
 * Puts the given player into the "fight ended" state.
 */
static void WaitForFightDone(Player *Play)
{
  if (HaveAbility(Play, A_DONEFIGHT)) {
    Play->EventNum = E_WAITDONE;
  } else {
    Play->EventNum = Play->ResyncNum;
    SendEvent(Play);
  }
}

/* 
 * Withdraws player "Play" from combat, and levies any penalties on
 * the player for this cowardly act, if applicable. If "ToLocation"
 * is >=0, then it identifies the location that the player is
 * trying to run to.
 */
void RunFromCombat(Player *Play, int ToLocation)
{
  int EscapeProb, RandNum;
  guint ArrayInd;
  gboolean FightingCop = FALSE;
  Player *Defend;
  char BackupAt;

  if (!Play || !Play->FightArray)
    return;

  EscapeProb = 60;

  /* Penalise players that are attacking others */
  if (Play->Attacking)
    EscapeProb /= 2;

  RandNum = brandom(0, 100);

  if (RandNum < EscapeProb) {
    if (!IsCop(Play) && brandom(0, 100) < 30) {
      for (ArrayInd = 0; ArrayInd < Play->FightArray->len; ArrayInd++) {
        Defend = (Player *)g_ptr_array_index(Play->FightArray, ArrayInd);
        if (IsCop(Defend)) {
          FightingCop = TRUE;
          break;
        }
      }
      if (FightingCop)
        Play->CopIndex--;
    }
    BackupAt = Play->IsAt;
    Play->IsAt = ToLocation;
    WithdrawFromCombat(Play);
    Play->IsAt = BackupAt;
    WaitForFightDone(Play);
  } else {
    SendFightMessage(Play, NULL, 0, F_FAILFLEE, (price_t)0, TRUE, NULL);
    AllowNextShooter(Play);
    if (FightTimeout)
      SetFightTimeout(Play);
    DoReturnFire(Play);
  }
}

void CheckForKilledPlayers(Player *Play)
{
  Player *Defend;
  guint ArrayInd;
  GPtrArray *KilledPlayers;

  KilledPlayers = g_ptr_array_new();
  for (ArrayInd = 0; ArrayInd < Play->FightArray->len; ArrayInd++) {
    Defend = (Player *)g_ptr_array_index(Play->FightArray, ArrayInd);

    if (Defend && Defend != Play && IsOpponent(Play, Defend) &&
        Defend->Health == 0) {
      g_ptr_array_add(KilledPlayers, (gpointer)Defend);
    }
  }
  for (ArrayInd = 0; ArrayInd < KilledPlayers->len; ArrayInd++) {
    Defend = (Player *)g_ptr_array_index(KilledPlayers, ArrayInd);
    WithdrawFromCombat(Defend);
    if (IsCop(Defend)) {
      if (!IsCop(Play))
        Play->CopIndex = -Defend->CopIndex;
      FirstServer = RemovePlayer(Defend, FirstServer);
    } else {
      FinishGame(Defend, _("You're dead! Game over."));
    }
  }

  g_ptr_array_free(KilledPlayers, TRUE);
}

/* 
 * If "Play" is attacking someone, and no cops are currently present,
 * then have the cops intervene (with a probability dependent on the
 * current location's PolicePresence)
 */
static void CheckCopsIntervene(Player *Play)
{
  guint ArrayInd;
  Player *Defend;

  if (!Play || !Play->FightArray || NumCop == 0 || NumGun == 0)
    return;                     /* Sanity check */

  if (!Play->Attacking)
    return;                     /* Cops don't attack "innocent victims" ;) */

  if (brandom(0, 100) > Location[Play->IsAt].PolicePresence) {
    return;                     /* The cops shouldn't _always_ attack
                                 * (unless P.P. == 100) */
  }

  for (ArrayInd = 0; Play->FightArray && ArrayInd < Play->FightArray->len;
       ArrayInd++) {
    Defend = (Player *)g_ptr_array_index(Play->FightArray, ArrayInd);
    if (IsCop(Defend))
      return;                   /* We don't want _more_ cops! */
  }

  /* OK - let 'em have it... */
  CopsAttackPlayer(Play);
}

/* 
 * Returns a suitable player (or cop) for "Play" to fire at. If "Play"
 * is attacking a designated target already, return that, otherwise
 * return the first valid opponent in the player's FightArray.
 */
static Player *GetFireTarget(Player *Play)
{
  Player *Defend;
  guint ArrayInd;

  if (Play->Attacking
      && g_slist_find(FirstServer, (gpointer)Play->Attacking)) {
    return Play->Attacking;
  } else {
    Play->Attacking = NULL;
    for (ArrayInd = 0; ArrayInd < Play->FightArray->len; ArrayInd++) {
      Defend = (Player *)g_ptr_array_index(Play->FightArray, ArrayInd);
      if (Defend && Defend != Play && IsOpponent(Play, Defend)) {
        return Defend;
      }
    }
  }
  return NULL;
}

static int GetArmor(Player *Play)
{
  int Armor;

  if (IsCop(Play)) {
    if (Play->Bitches.Carried == 0)
      Armor = Cop[Play->CopIndex - 1].Armor;
    else
      Armor = Cop[Play->CopIndex - 1].DeputyArmor;
  } else {
    if (Play->Bitches.Carried == 0)
      Armor = PlayerArmor;
    else
      Armor = BitchArmor;
  }
  if (Armor == 0)
    Armor = 1;
  return Armor;
}

/* 
 * Fires all weapons of player "Play" at an opponent, and resets
 * the fight timeout (the reload time).
 */
void Fire(Player *Play)
{
  int Damage, i, j;
  int AttackRating, DefendRating;
  int BitchesKilled;
  price_t Loot;
  FightPoint fp;
  Player *Defend;

  if (!Play->FightArray)
    return;
  if (!CanPlayerFire(Play))
    return;

  AllowNextShooter(Play);
  if (FightTimeout)
    SetFightTimeout(Play);

  Defend = GetFireTarget(Play);
  if (Defend) {
    Damage = 0;
    BitchesKilled = 0;
    Loot = 0;
    if (TotalGunsCarried(Play) > 0) {
      GetFightRatings(Play, Defend, &AttackRating, &DefendRating);
      if (brandom(0, AttackRating) > brandom(0, DefendRating)) {
        fp = F_HIT;
        for (i = 0; i < NumGun; i++)
          for (j = 0; j < Play->Guns[i].Carried; j++) {
            Damage += brandom(0, Gun[i].Damage);
          }
        Damage = Damage * 100 / GetArmor(Defend);
        if (Damage == 0)
          Damage = 1;
        HandleDamage(Defend, Play, Damage, &BitchesKilled, &Loot);
      } else
        fp = F_MISS;
    } else
      fp = F_STAND;
    SendFightMessage(Play, Defend, BitchesKilled, fp, Loot, TRUE, NULL);
  }
  CheckForKilledPlayers(Play);

  /* Careful, as we might have killed Player "Play" */
  if (g_slist_find(FirstServer, (gpointer)Play))
    DoReturnFire(Play);

  if (g_slist_find(FirstServer, (gpointer)Play))
    CheckCopsIntervene(Play);
}

gboolean CanPlayerFire(Player *Play)
{
  return (FightTimeout == 0 || Play->FightTimeout == 0 ||
          Play->FightTimeout <= time(NULL));
}

gboolean CanRunHere(Player *Play)
{
  return (Play->ResyncNum < E_ARRIVE && Play->ResyncNum != E_NONE);
}

/* 
 * To avoid boring waits, return the player who is next in line to be
 * able to shoot (i.e. with the smallest FightTimeout) so that this
 * player can be allowed to shoot immediately. If a player is already
 * eligible to shoot, or there is a tie for "first place" then do
 * nothing (i.e. return NULL).
 */
Player *GetNextShooter(Player *Play)
{
  Player *MinPlay, *Defend;
  time_t MinTimeout;
  guint mintie;
  guint ArrayInd;

  if (!FightTimeout)
    return NULL;

  MinPlay = NULL;
  MinTimeout = 0;
  mintie = 0;
  for (ArrayInd = 0; ArrayInd < Play->FightArray->len; ArrayInd++) {
    Defend = (Player *)g_ptr_array_index(Play->FightArray, ArrayInd);
    if (Defend == Play)
      continue;
    if (Defend->FightTimeout == 0)
      return NULL;
    if (MinTimeout == 0 || Defend->FightTimeout < MinTimeout
        || (Defend->FightTimeout == MinTimeout && Defend->tiebreak < mintie)) {
      MinPlay = Defend;
      MinTimeout = Defend->FightTimeout;
      mintie = Defend->tiebreak;
    }
  }

  return MinPlay;
}

void ResolveTipoff(Player *Play)
{
  GString *text;

  if (IsCop(Play) || !CanRunHere(Play))
    return;

  if (g_slist_find(FirstServer, (gpointer)Play->OnBehalfOf)) {
    dopelog(4, LF_SERVER, _("%s: tipoff by %s finished OK."),
            GetPlayerName(Play), GetPlayerName(Play->OnBehalfOf));
    RemoveListPlayer(&(Play->TipList), Play->OnBehalfOf);
    text = g_string_new("");
    if (Play->Health == 0) {
      g_string_printf(text,
                       _("Following your tipoff, the cops ambushed %s, "
                         "who was shot dead!"), GetPlayerName(Play));
    } else {
      dpg_string_printf(text,
                         _("Following your tipoff, the cops ambushed %s, "
                           "who escaped with %d %tde. "), GetPlayerName(Play),
                         Play->Bitches.Carried, Names.Bitches);
    }
    GainBitch(Play->OnBehalfOf);
    SendPlayerData(Play->OnBehalfOf);
    SendPrintMessage(NULL, C_NONE, Play->OnBehalfOf, text->str);
    g_string_free(text, TRUE);
  }
  Play->OnBehalfOf = NULL;
}

/* 
 * Cleans up combat after player "Play" has left.
 */
void WithdrawFromCombat(Player *Play)
{
  guint AttackInd, DefendInd;
  gboolean FightDone;
  Player *Attack, *Defend;
  GSList *list;
  gchar *text;

  for (list = FirstServer; list; list = g_slist_next(list)) {
    Attack = (Player *)list->data;
    if (Attack->Attacking == Play)
      Attack->Attacking = NULL;
  }

  if (!Play->FightArray)
    return;

  ResolveTipoff(Play);
  FightDone = TRUE;
  for (AttackInd = 0; AttackInd < Play->FightArray->len; AttackInd++) {
    Attack = (Player *)g_ptr_array_index(Play->FightArray, AttackInd);
    for (DefendInd = 0; DefendInd < AttackInd; DefendInd++) {
      Defend = (Player *)g_ptr_array_index(Play->FightArray, DefendInd);
      if (Attack != Play && Defend != Play && IsOpponent(Attack, Defend)) {
        FightDone = FALSE;
        break;
      }
    }
    if (!FightDone)
      break;
  }

  SendFightLeave(Play, FightDone);
  g_ptr_array_remove(Play->FightArray, (gpointer)Play);

  if (FightDone) {
    for (DefendInd = 0; DefendInd < Play->FightArray->len; DefendInd++) {
      Defend = (Player *)g_ptr_array_index(Play->FightArray, DefendInd);
      Defend->FightArray = NULL;
      ResolveTipoff(Defend);
      if (IsCop(Defend)) {
        FirstServer = RemovePlayer(Defend, FirstServer);
      } else if (Defend->Health == 0) {
        FinishGame(Defend, _("You're dead! Game over."));
      } else if (CanRunHere(Defend)
                 && brandom(0, 100) > Location[Defend->IsAt].PolicePresence) {
        Defend->EventNum = E_DOCTOR;
        Defend->DocPrice = prandom(Bitch.MinPrice, Bitch.MaxPrice) *
            Defend->Health / 500;
        text =
            dpg_strdup_printf(_
                              ("YN^Do you pay a doctor %P to sew you up?"),
                              Defend->DocPrice);
        SendQuestion(NULL, C_ASKSEW, Defend, text);
        g_free(text);
      } else {
        WaitForFightDone(Defend);
      }
    }
    g_ptr_array_free(Play->FightArray, TRUE);
  }
  Play->FightArray = NULL;
  Play->Attacking = NULL;
}

/* 
 * Inform player "To" of random offers or happenings. Returns 0 if
 * the client can immediately be advanced to the next state, or 1
 * there are first questions to be answered.
 */
int RandomOffer(Player *To)
{
  int r, amount, ind;
  GString *text;

  r = brandom(0, 100);

  text = g_string_new(NULL);

  if (!Sanitized && (r < 10)) {
    g_string_assign(text, _("You were mugged in the subway!"));
    To->Cash = To->Cash * (price_t)brandom(80, 95) / 100l;
  } else if (r < 30) {
    amount = brandom(3, 7);
    ind = IsCarryingRandom(To, amount);
    if (ind == -1 && amount > To->CoatSize) {
      g_string_free(text, TRUE);
      return 0;
    }
    if (ind == -1) {
      ind = brandom(0, NumDrug);
      dpg_string_printf(text,
                         _("You meet a friend! He gives you %d %tde."),
                         amount, Drug[ind].Name);
      To->Drugs[ind].Carried += amount;
      To->CoatSize -= amount;
    } else {
      dpg_string_printf(text,
                         _("You meet a friend! You give him %d %tde."),
                         amount, Drug[ind].Name);
      To->Drugs[ind].TotalValue =
          To->Drugs[ind].TotalValue * (To->Drugs[ind].Carried -
                                       amount) / To->Drugs[ind].Carried;
      To->Drugs[ind].Carried -= amount;
      To->CoatSize += amount;
    }
    SendPlayerData(To);
    SendPrintMessage(NULL, C_NONE, To, text->str);
  } else if (Sanitized) {
    /* Debugging message: we would normally have a random drug-related
       event here, but "Sanitized" mode is turned on */
    dopelog(3, LF_SERVER, _("Sanitized away a RandomOffer"));
  } else if (r < 50) {
    amount = brandom(3, 7);
    ind = IsCarryingRandom(To, amount);
    if (ind != -1) {
      dpg_string_printf(text, _("Police dogs chase you for %d blocks! "
                                 "You dropped some %tde! That's a drag, man!"),
                         brandom(3, 7), Names.Drugs);
      To->Drugs[ind].TotalValue = To->Drugs[ind].TotalValue *
          (To->Drugs[ind].Carried - amount) / To->Drugs[ind].Carried;
      To->Drugs[ind].Carried -= amount;
      To->CoatSize += amount;
      SendPlayerData(To);
      SendPrintMessage(NULL, C_NONE, To, text->str);
    } else {
      ind = brandom(0, NumDrug);
      amount = brandom(3, 7);
      if (amount > To->CoatSize) {
        g_string_free(text, TRUE);
        return 0;
      }
      dpg_string_printf(text,
                         _("You find %d %tde on a dead dude in the subway!"),
                         amount, Drug[ind].Name);
      To->Drugs[ind].Carried += amount;
      To->CoatSize -= amount;
      SendPlayerData(To);
      SendPrintMessage(NULL, C_NONE, To, text->str);
    }
  } else if (r < 60
             && To->Drugs[WEED].Carried + To->Drugs[HASHISH].Carried > 0) {
    ind = (To->Drugs[WEED].Carried >
           To->Drugs[HASHISH].Carried) ? WEED : HASHISH;
    amount = brandom(2, 6);
    if (amount > To->Drugs[ind].Carried)
      amount = To->Drugs[ind].Carried;
    dpg_string_printf(text,
                       _("Your mama made brownies with some of your %tde! "
                         "They were great!"), Drug[ind].Name);
    To->Drugs[ind].TotalValue = To->Drugs[ind].TotalValue *
        (To->Drugs[ind].Carried - amount) / To->Drugs[ind].Carried;
    To->Drugs[ind].Carried -= amount;
    To->CoatSize += amount;
    SendPlayerData(To);
    SendPrintMessage(NULL, C_NONE, To, text->str);
  } else if (r < 65) {
    g_string_assign(text,
                    _("YN^There is some weed that smells like paraquat "
                     "here!^It looks good! Will you smoke it? "));
    To->EventNum = E_WEED;
    SendQuestion(NULL, C_NONE, To, text->str);
    g_string_free(text, TRUE);
    return 1;
  } else if (NumStoppedTo > 0) {
    g_string_printf(text, _("You stopped to %s."),
                     StoppedTo[brandom(0, NumStoppedTo)]);
    amount = brandom(1, 10);
    if (To->Cash >= amount)
      To->Cash -= amount;
    SendPlayerData(To);
    SendPrintMessage(NULL, C_NONE, To, text->str);
  }
  g_string_free(text, TRUE);
  return 0;
}

/* 
 * Offers player "To" bitches/trenchcoats or guns. If ForceBitch is
 * TRUE, then a bitch is definitely offered. Returns 0 if the client
 * can advance immediately to the next state, 1 otherwise.
 */
int OfferObject(Player *To, gboolean ForceBitch)
{
  int ObjNum;
  gchar *text = NULL;

  if (brandom(0, 100) < 50 || ForceBitch) {
    if (WantAntique) {
      To->Bitches.Price = prandom(MINTRENCHPRICE, MAXTRENCHPRICE);
      text = dpg_strdup_printf(_("YN^Would you like to buy a bigger "
                                 "trenchcoat for %P?"), To->Bitches.Price);
    } else {
      To->Bitches.Price =
          prandom(Bitch.MinPrice, Bitch.MaxPrice) / (price_t)10;
      text =
          dpg_strdup_printf(_
                            ("YN^Hey dude! I'll help carry your %tde for a "
                             "mere %P. Yes or no?"), Names.Drugs,
                            To->Bitches.Price);
    }
    SendQuestion(NULL, C_ASKBITCH, To, text);
    g_free(text);
    return 1;
  } else if (!Sanitized && NumGun > 0
             && (TotalGunsCarried(To) < To->Bitches.Carried + 2)) {
    ObjNum = brandom(0, NumGun);
    To->Guns[ObjNum].Price = Gun[ObjNum].Price / 10;
    if (Gun[ObjNum].Space > To->CoatSize)
      return 0;
    text = dpg_strdup_printf(_("YN^Would you like to buy a %tde for %P?"),
                             Gun[ObjNum].Name, To->Guns[ObjNum].Price);
    SendQuestion(NULL, C_ASKGUN, To, text);
    g_free(text);
    return 1;
  }
  return 0;
}

/* Whether a particular drug is especially cheap or expensive */
enum DealType {
  DT_NORMAL, DT_CHEAP, DT_EXPENSIVE
};

/* 
 * Generates drug prices and drug busts etc. for player "To"
 * "Deal" is an array of size NumDrug.
 */
static void GenerateDrugsHere(Player *To, enum DealType *Deal)
{
  int NumEvents, NumDrugs, NumRandom, i;

  for (i = 0; i < NumDrug; i++) {
    To->Drugs[i].Price = 0;
    Deal[i] = DT_NORMAL;
  }
  NumEvents = 0;
  if (brandom(0, 100) < 70)
    NumEvents = 1;
  if (brandom(0, 100) < 40 && NumEvents == 1)
    NumEvents = 2;
  if (brandom(0, 100) < 5 && NumEvents == 2)
    NumEvents = 3;
  NumDrugs = 0;
  while (NumEvents > 0) {
    i = brandom(0, NumDrug);
    if (Deal[i] != DT_NORMAL)
      continue;
    if (Drug[i].Expensive && (!Drug[i].Cheap || brandom(0, 100) < 50)) {
      Deal[i] = DT_EXPENSIVE;
      To->Drugs[i].Price = prandom(Drug[i].MinPrice, Drug[i].MaxPrice)
          * Drugs.ExpensiveMultiply;
      NumDrugs++;
      NumEvents--;
    } else if (Drug[i].Cheap) {
      Deal[i] = DT_CHEAP;
      To->Drugs[i].Price = prandom(Drug[i].MinPrice, Drug[i].MaxPrice)
          / Drugs.CheapDivide;
      NumDrugs++;
      NumEvents--;
    }
  }
  NumRandom = brandom(Location[To->IsAt].MinDrug,
                      Location[To->IsAt].MaxDrug);
  if (NumRandom > NumDrug)
    NumRandom = NumDrug;

  NumDrugs = NumRandom - NumDrugs;
  while (NumDrugs > 0) {
    i = brandom(0, NumDrug);
    if (To->Drugs[i].Price == 0) {
      To->Drugs[i].Price = prandom(Drug[i].MinPrice, Drug[i].MaxPrice);
      NumDrugs--;
    }
  }
}

/* 
 * Sends details of drug prices to player "To". If "DisplayBusts"
 * is TRUE, also regenerates drug prices and sends details of
 * special events such as drug busts.
 */
void SendDrugsHere(Player *To, gboolean DisplayBusts)
{
  int i;
  enum DealType *Deal = NULL;
  gchar *prstr;
  GString *text;
  gboolean First;

  Deal = g_new0(enum DealType, NumDrug);
  if (DisplayBusts)
    GenerateDrugsHere(To, Deal);

  text = g_string_new(NULL);
  First = TRUE;
  if (DisplayBusts) {
    for (i = 0; i < NumDrug; i++) {
      if (Deal[i] != DT_NORMAL) {
        if (!First)
          g_string_append_c(text, '^');
        if (Deal[i] == DT_CHEAP) {
          g_string_append(text, Drug[i].CheapStr);
        } else {
          dpg_string_append_printf(text, brandom(0, 100) < 50
                              ? Drugs.ExpensiveStr1 : Drugs.ExpensiveStr2,
                              Drug[i].Name);
        }
        First = FALSE;
      }
    }
  }
  g_free(Deal);

  if (!First)
    SendPrintMessage(NULL, C_NONE, To, text->str);
  g_string_truncate(text, 0);
  for (i = 0; i < NumDrug; i++) {
    g_string_append_printf(text, "%s^",
                      (prstr = pricetostr(To->Drugs[i].Price)));
    g_free(prstr);
  }
  SendServerMessage(NULL, C_NONE, C_DRUGHERE, To, text->str);
  g_string_free(text, TRUE);
}

/* 
 * Handles the incoming message in "answer" from player "From" and
 * intended for player "To".
 */
void HandleAnswer(Player *From, Player *To, char *answer)
{
  int i;
  gchar *text;
  Player *Defender;

  if (!From || From->EventNum == E_NONE)
    return;
  if (answer[0] == 'Y' && From->EventNum == E_OFFOBJECT
      && From->Bitches.Price && From->Bitches.Price > From->Cash)
    answer[0] = 'N';
  if ((From->EventNum == E_FIGHT || From->EventNum == E_FIGHTASK)
      && CanRunHere(From)) {
    From->EventNum = E_FIGHT;
    if (answer[0] == 'R' || answer[0] == 'Y') {
      RunFromCombat(From, -1);
    } else {
      Fire(From);
    }
  } else if (answer[0] == 'Y')
    switch (From->EventNum) {
    case E_OFFOBJECT:
      if (g_slist_find(FirstServer, (gpointer)From->OnBehalfOf)) {
        dopelog(3, LF_SERVER, _("%s: offer was on behalf of %s"),
                GetPlayerName(From), GetPlayerName(From->OnBehalfOf));
        if (From->Bitches.Price) {
          text = dpg_strdup_printf(_("%s has accepted your %tde!"
                                     "^Use the G key to contact your spy."),
                                   GetPlayerName(From), Names.Bitch);
          From->OnBehalfOf->Flags |= SPYINGON;
          SendPlayerData(From->OnBehalfOf);
          SendPrintMessage(NULL, C_NONE, From->OnBehalfOf, text);
          g_free(text);
          i = GetListEntry(&(From->SpyList), From->OnBehalfOf);
          if (i >= 0)
            From->SpyList.Data[i].Turns = 0;
        }
      }
      if (From->Bitches.Price) {
        text = g_strdup_printf("bitch^0^1");
        BuyObject(From, text);
        g_free(text);
      } else {
        for (i = 0; i < NumGun; i++)
          if (From->Guns[i].Price) {
            text = g_strdup_printf("gun^%d^1", i);
            BuyObject(From, text);
            g_free(text);
            break;
          }
      }
      From->OnBehalfOf = NULL;
      From->EventNum++;
      SendEvent(From);
      break;
    case E_LOANSHARK:
      SendServerMessage(NULL, C_NONE, C_LOANSHARK, From, NULL);
      break;
    case E_BANK:
      SendServerMessage(NULL, C_NONE, C_BANK, From, NULL);
      break;
    case E_GUNSHOP:
      for (i = 0; i < NumGun; i++)
        From->Guns[i].Price = Gun[i].Price;
      SendServerMessage(NULL, C_NONE, C_GUNSHOP, From, NULL);
      break;
    case E_HIREBITCH:
      text = g_strdup_printf("bitch^0^1");
      BuyObject(From, text);
      g_free(text);
      From->EventNum++;
      SendEvent(From);
      break;
    case E_ROUGHPUB:
      From->EventNum++;
      SendEvent(From);
      break;
    case E_WEED:
      FinishGame(From, _("You hallucinated for three days on the wildest "
                         "trip you ever imagined!^Then you died because "
                         "your brain disintegrated!"));
      break;
    case E_DOCTOR:
      if (From->Cash >= From->DocPrice) {
        From->Cash -= From->DocPrice;
        From->Health = 100;
        SendPlayerData(From);
      }
      WaitForFightDone(From);
      break;
    default:
      break;
  } else if (From->EventNum == E_ARRIVE) {
    if ((answer[0] == 'A' || answer[0] == 'T') &&
        IsConnectedPlayer(From->OnBehalfOf) &&
        g_slist_find(FirstServer, (gpointer)From->OnBehalfOf)) {
      Defender = From->OnBehalfOf;
      From->OnBehalfOf = NULL;  /* So we don't think it was a tipoff */
      if (Defender->IsAt == From->IsAt) {
        if (answer[0] == 'A') {
          From->EventNum = Defender->EventNum = E_NONE;
          AttackPlayer(From, Defender);
        }
      } else {
        text = g_strdup_printf(_("Too late - %s has just left!"),
                               GetPlayerName(Defender));
        SendPrintMessage(NULL, C_MISSFIGHT, From, text);
        g_free(text);
        From->EventNum++;
        SendEvent(From);
      }
    } else {
      From->EventNum++;
      SendEvent(From);
    }
    From->OnBehalfOf = NULL;
  } else
    switch (From->EventNum) {
    case E_ROUGHPUB:
      From->EventNum++;
      From->EventNum++;
      SendEvent(From);
      break;
    case E_DOCTOR:
      WaitForFightDone(From);
      break;
    case E_HIREBITCH:
    case E_GUNSHOP:
    case E_BANK:
    case E_LOANSHARK:
    case E_OFFOBJECT:
    case E_WEED:
      if (g_slist_find(FirstServer, (gpointer)From->OnBehalfOf)) {
        dopelog(3, LF_SERVER, _("%s: offer was on behalf of %s"),
                GetPlayerName(From), GetPlayerName(From->OnBehalfOf));
        if (From->Bitches.Price && From->EventNum == E_OFFOBJECT) {
          text = dpg_strdup_printf(_("%s has rejected your %tde!"),
                                   GetPlayerName(From), Names.Bitch);
          GainBitch(From->OnBehalfOf);
          SendPlayerData(From->OnBehalfOf);
          SendPrintMessage(NULL, C_NONE, From->OnBehalfOf, text);
          g_free(text);
          RemoveListPlayer(&(From->SpyList), From->OnBehalfOf);
        }
      }
      From->EventNum++;
      SendEvent(From);
      break;
    default:
      break;
    }
}

/* 
 * Processes a request stored in "data" from player "From" to buy an
 * object (bitch, gun, or drug).
 * Objects can be sold if the amount given in "data" is negative, and
 * given away if their current price is zero.
 */
void BuyObject(Player *From, char *data)
{
  char *cp, *type;
  int index, i, amount;

  cp = data;
  type = GetNextWord(&cp, "");
  index = GetNextInt(&cp, 0);
  amount = GetNextInt(&cp, 0);
  if (strcmp(type, "drug") == 0) {
    if (index >= 0 && index < NumDrug
        && From->Drugs[index].Carried + amount >= 0
        && From->CoatSize - amount >= 0 && (From->Drugs[index].Price != 0
                                            || amount < 0)
        && From->Cash >= amount * From->Drugs[index].Price) {
      if (amount > 0) {
        From->Drugs[index].TotalValue += amount * From->Drugs[index].Price;
      } else if (From->Drugs[index].Carried != 0) {
        From->Drugs[index].TotalValue = From->Drugs[index].TotalValue *
            (From->Drugs[index].Carried +
             amount) / From->Drugs[index].Carried;
      }
      From->Drugs[index].Carried += amount;
      From->CoatSize -= amount;
      From->Cash -= amount * From->Drugs[index].Price;
      SendPlayerData(From);

      if (!Sanitized && NumCop > 0 && NumGun > 0
          && (From->Drugs[index].Price == 0 &&
              brandom(0, 100) < Location[From->IsAt].PolicePresence)) {
        gchar *text;

        text = dpg_strdup_printf(_("The cops spot you dropping %tde!"),
                                 Names.Drugs);
        SendPrintMessage(NULL, C_NONE, From, text);
        g_free(text);
        CopsAttackPlayer(From);
      }
    }
  } else if (strcmp(type, "gun") == 0) {
    if (index >= 0 && index < NumGun
        && TotalGunsCarried(From) + amount >= 0
        && TotalGunsCarried(From) + amount <= From->Bitches.Carried + 2
        && From->Guns[index].Price != 0
        && From->CoatSize - amount * Gun[index].Space >= 0
        && From->Cash >= amount * From->Guns[index].Price) {
      From->Guns[index].Carried += amount;
      From->CoatSize -= amount * Gun[index].Space;
      From->Cash -= amount * From->Guns[index].Price;
      SendPlayerData(From);
    }
  } else if (strcmp(type, "bitch") == 0) {
    if (From->Bitches.Carried + amount >= 0
        && From->Bitches.Price != 0
        && amount * From->Bitches.Price <= From->Cash) {
      for (i = 0; i < amount; i++)
        GainBitch(From);
      if (amount > 0)
        From->Cash -= amount * From->Bitches.Price;
      SendPlayerData(From);
    }
  }
}

/* 
 * Clears the bitch and gun prices stored for player "Play".
 */
void ClearPrices(Player *Play)
{
  int i;

  Play->Bitches.Price = 0;
  for (i = 0; i < NumGun; i++)
    Play->Guns[i].Price = 0;
}

/* 
 * Gives player "Play" a new bitch (or larger trenchcoat).
 */
void GainBitch(Player *Play)
{
  Play->CoatSize += 10;
  Play->Bitches.Carried++;
}

/* 
 * Loses one bitch belonging to player "Play". If drugs or guns are
 * lost with the bitch, 1 is returned (0 otherwise) and the lost
 * items are added to "Guns" and "Drugs" if non-NULL.
 */
int LoseBitch(Player *Play, Inventory *Guns, Inventory *Drugs)
{
  int losedrug = 0, i, num, drugslost;
  int *GunIndex, tmp;
  GunIndex = g_new(int, NumGun);

  ClearInventory(Guns, Drugs);
  Play->CoatSize -= 10;
  if (TotalGunsCarried(Play) > 0) {
    if (brandom(0, 100) <
        TotalGunsCarried(Play) * 100 / (Play->Bitches.Carried + 2)) {
      for (i = 0; i < NumGun; i++)
        GunIndex[i] = i;
      for (i = 0; i < NumGun * 5; i++) {
        num = brandom(0, NumGun - 1);
        tmp = GunIndex[num + 1];
        GunIndex[num + 1] = GunIndex[num];
        GunIndex[num] = tmp;
      }
      for (i = 0; i < NumGun; i++) {
        if (Play->Guns[GunIndex[i]].Carried > 0) {
          Play->Guns[GunIndex[i]].Carried--;
          losedrug = 1;
          Play->CoatSize += Gun[GunIndex[i]].Space;
          if (Guns)
            Guns[GunIndex[i]].Carried++;
          break;
        }
      }
    }
  }
  for (i = 0; i < NumDrug; i++)
    if (Play->Drugs[i].Carried > 0) {
      num =
          (int)((float)Play->Drugs[i].Carried /
                (Play->Bitches.Carried + 2.0) + 0.5);
      if (num > 0) {
        Play->Drugs[i].TotalValue = Play->Drugs[i].TotalValue *
            (Play->Drugs[i].Carried - num) / Play->Drugs[i].Carried;
        Play->Drugs[i].Carried -= num;
        if (Drugs)
          Drugs[i].Carried += num;
        Play->CoatSize += num;
        losedrug = 1;
      }
    }
  while (Play->CoatSize < 0) {
    drugslost = 0;
    for (i = 0; i < NumDrug; i++) {
      if (Play->Drugs[i].Carried > 0) {
        losedrug = 1;
        drugslost = 1;
        Play->Drugs[i].TotalValue = Play->Drugs[i].TotalValue *
            (Play->Drugs[i].Carried - 1) / Play->Drugs[i].Carried;
        Play->Drugs[i].Carried--;
        Play->CoatSize++;
        if (Play->CoatSize >= 0)
          break;
      }
    }
    if (!drugslost)
      for (i = 0; i < NumGun; i++) {
        if (Play->Guns[i].Carried > 0) {
          losedrug = 1;
          Play->Guns[i].Carried--;
          Play->CoatSize += Gun[i].Space;
          if (Play->CoatSize >= 0)
            break;
        }
      }
  }
  Play->Bitches.Carried--;
  g_free(GunIndex);
  return losedrug;
}

/* 
 * If fight timeouts are in force, sets the timeout for the given player.
 */
void SetFightTimeout(Player *Play)
{
  if (FightTimeout) {
    Play->FightTimeout = time(NULL) + (time_t) FightTimeout;

    /* Make sure we have a higher tiebreak value than any other player with
     * the same fight timeout (since FightTimeout only has second resolution,
     * and possibly only microseconds have elapsed) */
    Play->tiebreak = 0;
    if (Play->FightTimeout) {
      GSList *listpt;

      for (listpt = FirstServer; listpt; listpt = g_slist_next(listpt)) {
        Player *listplay = (Player *)listpt->data;

        if (listplay && listplay != Play
            && listplay->FightTimeout == Play->FightTimeout) {
          Play->tiebreak = MAX(Play->tiebreak, listplay->tiebreak + 1);
        }
      }
    }
  } else {
    Play->FightTimeout = 0;
  }
}

/* 
 * Removes any fight timeout for the given player.
 */
void ClearFightTimeout(Player *Play)
{
  Play->FightTimeout = 0;
}

/* 
 * Given the time of a pending event in "timeout" and the current time in
 * "timenow", updates "mintime" with the number of seconds to that event,
 * unless "mintime" is already smaller (as long as it's not -1, which
 * means "uninitialized"). Returns 1 if the timeout has already expired.
 */
long AddTimeout(time_t timeout, time_t timenow, long *mintime)
{
  if (timeout == 0)
    return 0;
  else if (timeout <= timenow)
    return 1;
  else {
    if (*mintime < 0 || timeout - timenow < *mintime)
      *mintime = timeout - timenow;
    return 0;
  }
}

/* 
 * Returns the number of seconds until the next scheduled event. If such
 * an event has already expired, returns 0. If no events are pending,
 * returns -1. "First" should point to a list of valid players.
 */
long GetMinimumTimeout(GSList *First)
{
  Player *Play;
  GSList *list;
  long mintime = -1;
  time_t timenow;

  timenow = time(NULL);
#ifdef NETWORKING
  curl_multi_timeout(MetaConn.multi, &mintime);
#endif
  if (AddTimeout(MetaMinTimeout, timenow, &mintime))
    return 0;
  if (AddTimeout(MetaUpdateTimeout, timenow, &mintime))
    return 0;
  for (list = First; list; list = g_slist_next(list)) {
    Play = (Player *)list->data;
    if (AddTimeout(Play->FightTimeout, timenow, &mintime) ||
        AddTimeout(Play->IdleTimeout, timenow, &mintime) ||
        AddTimeout(Play->ConnectTimeout, timenow, &mintime))
      return 0;
  }
  return mintime;
}

/* 
 * Given a list of players in "First", checks to see if any events
 * have timed out, and if so, performs the necessary actions. The
 * new start of the list is returned, since a player may be removed
 * if their ConnectTimeout has expired.
 */
GSList *HandleTimeouts(GSList *First)
{
  GSList *list, *nextlist;
  Player *Play;
  time_t timenow;

  timenow = time(NULL);
  if (MetaMinTimeout <= timenow) {
    MetaMinTimeout = 0;
    if (MetaPlayerPending) {
      dopelog(3, LF_SERVER, _("Sending pending updates to the metaserver..."));
      RegisterWithMetaServer(TRUE, TRUE, FALSE);
    }
  }
  if (MetaUpdateTimeout != 0 && MetaUpdateTimeout <= timenow) {
    dopelog(3, LF_SERVER, _("Sending reminder message to the metaserver..."));
    RegisterWithMetaServer(TRUE, FALSE, FALSE);
  }
  list = First;
  while (list) {
    nextlist = g_slist_next(list);
    Play = (Player *)list->data;
    if (Play->IdleTimeout != 0 && Play->IdleTimeout <= timenow) {
      Play->IdleTimeout = 0;
      dopelog(1, LF_SERVER, _("Player removed due to idle timeout"));
      SendPrintMessage(NULL, C_NONE, Play,
                       "Disconnected due to idle timeout");
      ClientLeftServer(Play);
      /* Blank the name, so that CountPlayers ignores this player */
      SetPlayerName(Play, NULL);
      /* Make sure they do actually disconnect, eventually! */
      if (ConnectTimeout) {
        Play->ConnectTimeout = time(NULL) + (time_t) ConnectTimeout;
      }
    } else if (Play->ConnectTimeout != 0
               && Play->ConnectTimeout <= timenow) {
      Play->ConnectTimeout = 0;
      dopelog(1, LF_SERVER, _("Player removed due to connect timeout"));
      First = RemovePlayer(Play, First);
    } else if (IsConnectedPlayer(Play) &&
               Play->FightTimeout != 0 && Play->FightTimeout <= timenow) {
      ClearFightTimeout(Play);
      if (IsCop(Play))
        Fire(Play);
      else
        SendFightReload(Play);
    }
    list = nextlist;
  }
  return First;
}
