/* Emacs style mode select   -*- C++ -*- */
/*-----------------------------------------------------------------------------*/

/* $Id:$*/

/* Copyright (C) 1993-1996 by id Software, Inc.*/

/* This source is available for distribution and/or modification*/
/* only under the terms of the DOOM Source Code License as*/
/* published by id Software. All rights reserved.*/

/* The source is distributed in the hope that it will be useful,*/
/* but WITHOUT ANY WARRANTY; without even the implied warranty of*/
/* FITNESS FOR A PARTICULAR PURPOSE. See the DOOM Source Code License*/
/* for more details.*/

/* $Log:$*/

/* DESCRIPTION:*/
/*	DOOM Network game communication and protocol,*/
/*	all OS independend parts.*/

/*-----------------------------------------------------------------------------*/


static const char rcsid[] = "$Id: d_net.c,v 1.3 1997/02/03 22:01:47 b1 Exp $";


#include "m_menu.h"
#include "i_system.h"
#include "i_video.h"
#include "i_net.h"
#include "g_game.h"
#include "d_main.h"
#include "r_main.h"
#include "doomdef.h"
#include "doomstat.h"

#define	NCMD_EXIT		0x80000000
#define	NCMD_RETRANSMIT		0x40000000
#define	NCMD_SETUP		0x20000000
#define	NCMD_KILL		0x10000000	/* kill game*/
#define	NCMD_CHECKSUM	 	0x0fffffff


doomcom_t*	doomcom;
doomdata_t*	netbuffer;		/* points inside doomcom*/



/* NETWORKING*/

/* gametic is the tic about to (or currently being) run*/
/* maketic is the tick that hasn't had control made for it yet*/
/* nettics[] has the maketics for all players */

/* a gametic cannot be run until nettics[] > gametic for all players*/

#define	RESENDCOUNT	10
#define	PL_DRONE	0x80	/* bit flag in doomdata->player*/

static ticcmd_t	localcmds[BACKUPTICS];

ticcmd_t	netcmds[MAXPLAYERS][BACKUPTICS];
static int	nettics[MAXNETNODES];
static boolean	nodeingame[MAXNETNODES];		/* set false as nodes leave game*/
static boolean	remoteresend[MAXNETNODES];		/* set when local needs tics*/
static int	resendto[MAXNETNODES];			/* set when remote needs tics*/
static int	resendcount[MAXNETNODES];

static int	nodeforplayer[MAXPLAYERS];

int             maketic = 0;
/*static int	lastnettic;*/
static int	skiptics;
int		ticdup;
static int	maxsend;	/* BACKUPTICS/(2*ticdup)-1*/


static boolean		reboundpacket;
static doomdata_t	reboundstore;






static int NetbufferSize (void)
{
    return (int)&(((doomdata_t *)0)->cmds[netbuffer->numtics]);
}


/* Checksum */

static unsigned NetbufferChecksum (void)
{
    unsigned		c;
    int		i,l;

    c = 0x1234567;

    /* FIXME -endianess?*/
#ifdef NORMALUNIX
    return 0;			/* byte order problems*/
#endif

    l = (NetbufferSize () - (int)&(((doomdata_t *)0)->retransmitfrom))/4;
    for (i=0 ; i<l ; i++)
	c += ((unsigned *)&netbuffer->retransmitfrom)[i] * (i+1);

    return c & NCMD_CHECKSUM;
}




static int ExpandTics (int low)
{
    int	delta;

    delta = low - (maketic&0xff);

    if (delta >= -64 && delta <= 64)
	return (maketic&~0xff) + low;
    if (delta > 64)
	return (maketic&~0xff) - 256 + low;
    if (delta < -64)
	return (maketic&~0xff) + 256 + low;

    I_Error ("ExpandTics: strange value %i at maketic %i",low,maketic);
    return 0;
}




/* HSendPacket*/

static void
HSendPacket
 (int	node,
  int	flags )
{
    netbuffer->checksum = NetbufferChecksum () | flags;

    if (!node)
    {
	reboundstore = *netbuffer;
	reboundpacket = true;
	return;
    }

    if (demoplayback)
	return;

    if (!netgame)
	I_Error ("Tried to transmit to another node");

    doomcom->command = CMD_SEND;
    doomcom->remotenode = node;
    doomcom->datalength = NetbufferSize ();

    if (debugfile)
    {
	int		i;
	int		realretrans;
	if (netbuffer->checksum & NCMD_RETRANSMIT)
	    realretrans = ExpandTics (netbuffer->retransmitfrom);
	else
	    realretrans = -1;

	fprintf (debugfile,"send (%i + %i, R %i) [%i] ",
		 ExpandTics(netbuffer->starttic),
		 netbuffer->numtics, realretrans, doomcom->datalength);

	for (i=0 ; i<doomcom->datalength ; i++)
	    fprintf (debugfile,"%i ",((byte *)netbuffer)[i]);

	fprintf (debugfile,"\n");
    }

    I_NetCmd ();
}


/* HGetPacket*/
/* Returns false if no packet is waiting*/

static boolean HGetPacket (void)
{
    if (reboundpacket)
    {
	*netbuffer = reboundstore;
	doomcom->remotenode = 0;
	reboundpacket = false;
	return true;
    }

    if (!netgame)
	return false;

    if (demoplayback)
	return false;

    doomcom->command = CMD_GET;
    I_NetCmd ();

    if (doomcom->remotenode == -1)
	return false;

    if (doomcom->datalength != NetbufferSize ())
    {
	if (debugfile)
	    fprintf (debugfile,"bad packet length %i\n",doomcom->datalength);
	return false;
    }

    if (NetbufferChecksum () != (netbuffer->checksum&NCMD_CHECKSUM) )
    {
	if (debugfile)
	    fprintf (debugfile,"bad packet checksum\n");
	return false;
    }

    if (debugfile)
    {
	int		realretrans;
	int	i;

	if (netbuffer->checksum & NCMD_SETUP)
	    fprintf (debugfile,"setup packet\n");
	else
	{
	    if (netbuffer->checksum & NCMD_RETRANSMIT)
		realretrans = ExpandTics (netbuffer->retransmitfrom);
	    else
		realretrans = -1;

	    fprintf (debugfile,"get %i = (%i + %i, R %i)[%i] ",
		     doomcom->remotenode,
		     ExpandTics(netbuffer->starttic),
		     netbuffer->numtics, realretrans, doomcom->datalength);

	    for (i=0 ; i<doomcom->datalength ; i++)
		fprintf (debugfile,"%i ",((byte *)netbuffer)[i]);
	    fprintf (debugfile,"\n");
	}
    }
    return true;
}



/* GetPackets*/

static char    exitmsg[80];

static void GetPackets (void)
{
    int		netconsole;
    int		netnode;
    ticcmd_t	*src, *dest;
    int		realend;
    int		realstart;

    while ( HGetPacket() )
    {
	if (netbuffer->checksum & NCMD_SETUP)
	    continue;		/* extra setup packet*/

	netconsole = netbuffer->player & ~PL_DRONE;
	netnode = doomcom->remotenode;

	/* to save bytes, only the low byte of tic numbers are sent*/
	/* Figure out what the rest of the bytes are*/
	realstart = ExpandTics (netbuffer->starttic);
	realend = (realstart+netbuffer->numtics);

	/* check for exiting the game*/
	if (netbuffer->checksum & NCMD_EXIT)
	{
	    if (!nodeingame[netnode])
		continue;
	    nodeingame[netnode] = false;
	    playeringame[netconsole] = false;
	    strcpy (exitmsg, "Player 1 left the game");
	    exitmsg[7] += netconsole;
	    players[consoleplayer].message = exitmsg;
	    if (demorecording)
		G_CheckDemoStatus ();
	    continue;
	}

	/* check for a remote game kill*/
	if (netbuffer->checksum & NCMD_KILL)
	    I_Error ("Killed by network driver");

	nodeforplayer[netconsole] = netnode;

	/* check for retransmit request*/
	if ( resendcount[netnode] <= 0
	     && (netbuffer->checksum & NCMD_RETRANSMIT) )
	{
	    resendto[netnode] = ExpandTics(netbuffer->retransmitfrom);
	    if (debugfile)
		fprintf (debugfile,"retransmit from %i\n", resendto[netnode]);
	    resendcount[netnode] = RESENDCOUNT;
	}
	else
	    resendcount[netnode]--;

	/* check for out of order / duplicated packet		*/
	if (realend == nettics[netnode])
	    continue;

	if (realend < nettics[netnode])
	{
	    if (debugfile)
		fprintf (debugfile,
			 "out of order packet (%i + %i)\n" ,
			 realstart,netbuffer->numtics);
	    continue;
	}

	/* check for a missed packet*/
	if (realstart > nettics[netnode])
	{
	    /* stop processing until the other system resends the missed tics*/
	    if (debugfile)
		fprintf (debugfile,
			 "missed tics from %i (%i - %i)\n",
			 netnode, realstart, nettics[netnode]);
	    remoteresend[netnode] = true;
	    continue;
	}

	/* update command store from the packet*/
        {
	    int		start;

	    remoteresend[netnode] = false;

	    start = nettics[netnode] - realstart;
	    src = &netbuffer->cmds[start];

	    while (nettics[netnode] < realend)
	    {
		dest = &netcmds[netconsole][nettics[netnode]%BACKUPTICS];
		nettics[netnode]++;
		*dest = *src;
		src++;
	    }
	}
    }
}



/* NetUpdate*/
/* Builds ticcmds for console player,*/
/* sends out a packet*/

static int      gametime;

void NetUpdate (void)
{
    int         nowtime;
    int         newtics;
    int		i,j;
    int		realstart;
    int		gameticdiv;

    /* check time*/
    nowtime = I_GetTime ()/ticdup;
    newtics = nowtime - gametime;
    gametime = nowtime;

    if (newtics <= 0) 	/* nothing new to update*/
	goto listen;

    if (skiptics <= newtics)
    {
	newtics -= skiptics;
	skiptics = 0;
    }
    else
    {
	skiptics -= newtics;
	newtics = 0;
    }


    netbuffer->player = consoleplayer;

    /* build new ticcmds for console player*/
    gameticdiv = gametic/ticdup;
    for (i=0 ; i<newtics ; i++)
    {
	I_StartTic ();
	D_ProcessEvents ();
	if (maketic - gameticdiv >= BACKUPTICS/2-1)
	    break;          /* can't hold any more*/

	/*printf ("mk:%i ",maketic);*/
	G_BuildTiccmd (&localcmds[maketic%BACKUPTICS]);
	maketic++;
    }


    if (singletics)
	return;         /* singletic update is syncronous*/

    /* send the packet to the other nodes*/
    for (i=0 ; i<doomcom->numnodes ; i++)
	if (nodeingame[i])
	{
	    netbuffer->starttic = realstart = resendto[i];
	    netbuffer->numtics = maketic - realstart;
	    if (netbuffer->numtics > BACKUPTICS)
		I_Error ("NetUpdate: netbuffer->numtics > BACKUPTICS");

	    resendto[i] = maketic - doomcom->extratics;

	    for (j=0 ; j< netbuffer->numtics ; j++)
		netbuffer->cmds[j] =
		    localcmds[(realstart+j)%BACKUPTICS];

	    if (remoteresend[i])
	    {
		netbuffer->retransmitfrom = nettics[i];
		HSendPacket (i, NCMD_RETRANSMIT);
	    }
	    else
	    {
		netbuffer->retransmitfrom = 0;
		HSendPacket (i, 0);
	    }
	}

    /* listen for other packets*/
  listen:
    GetPackets ();
}




/* CheckAbort*/

static void CheckAbort (void)
{
    event_t *ev;
    int		stoptic;

    stoptic = I_GetTime () + 2;
    while (I_GetTime() < stoptic)
	I_StartTic ();

    I_StartTic ();
    for ( ; eventtail != eventhead
	      ; ++eventtail, eventtail = (eventtail)&(MAXEVENTS-1) )
    {
	ev = &events[eventtail];
	if (ev->type == ev_keydown && ev->data1 == KEY_ESCAPE)
	    I_Error ("Network game synchronization aborted.");
    }
}



/* D_ArbitrateNetStart*/

static void D_ArbitrateNetStart (void)
{
    int		i;
    boolean	gotinfo[MAXNETNODES];

    autostart = true;
    memset (gotinfo,0,sizeof(gotinfo));

    if (doomcom->consoleplayer)
    {
	/* listen for setup info from key player*/
	fprintf (logfile, "listening for network start info...\n");
	while (1)
	{
	    CheckAbort ();
	    if (!HGetPacket ())
		continue;
	    if (netbuffer->checksum & NCMD_SETUP)
	    {
		if (netbuffer->player != VERSION)
		    I_Error ("Different DOOM versions cannot play a net game!");
		startskill = (skill_t)(netbuffer->retransmitfrom & 15);
		deathmatch = (netbuffer->retransmitfrom & 0xc0) >> 6;
		nomonsters = (netbuffer->retransmitfrom & 0x20) > 0;
		respawnparm = (netbuffer->retransmitfrom & 0x10) > 0;
		startmap = netbuffer->starttic & 0x3f;
		startepisode = netbuffer->starttic >> 6;
		return;
	    }
	}
    }
    else
    {
	/* key player, send the setup info*/
	fprintf (logfile, "sending network start info...\n");
	do
	{
	    CheckAbort ();
	    for (i=0 ; i<doomcom->numnodes ; i++)
	    {
		netbuffer->retransmitfrom = startskill;
		if (deathmatch)
		    netbuffer->retransmitfrom |= (deathmatch<<6);
		if (nomonsters)
		    netbuffer->retransmitfrom |= 0x20;
		if (respawnparm)
		    netbuffer->retransmitfrom |= 0x10;
		netbuffer->starttic = startepisode * 64 + startmap;
		netbuffer->player = VERSION;
		netbuffer->numtics = 0;
		HSendPacket (i, NCMD_SETUP);
	    }

#if 1
	    for(i = 10 ; i  &&  HGetPacket(); --i)
	    {
		if((netbuffer->player&0x7f) < MAXNETNODES)
		    gotinfo[netbuffer->player&0x7f] = true;
	    }
#else
	    while (HGetPacket ())
	    {
		gotinfo[netbuffer->player&0x7f] = true;
	    }
#endif

	    for (i=1 ; i<doomcom->numnodes ; i++)
		if (!gotinfo[i])
		    break;
	} while (i < doomcom->numnodes);
    }
}


/* D_CheckNetGame*/
/* Works out player numbers among the net participants*/

void D_CheckNetGame (void)
{
    int             i;

    for (i=0 ; i<MAXNETNODES ; i++)
    {
	nodeingame[i] = false;
       	nettics[i] = 0;
	remoteresend[i] = false;	/* set when local needs tics*/
	resendto[i] = 0;		/* which tic to start sending*/
    }

    gametic=maketic=0;
    I_TimeBase();

    /* I_InitNetwork sets doomcom and netgame*/
    I_InitNetwork ();
    if (doomcom->id != DOOMCOM_ID)
	I_Error ("Doomcom buffer invalid!");

    netbuffer = &doomcom->data;
    consoleplayer = displayplayer = doomcom->consoleplayer;
    if (netgame)
	D_ArbitrateNetStart ();

    fprintf (logfile, "startskill %i  deathmatch: %i  startmap: %i  startepisode: %i\n",
	    startskill, deathmatch, startmap, startepisode);

    /* read values out of doomcom*/
    ticdup = doomcom->ticdup;
    maxsend = BACKUPTICS/(2*ticdup)-1;
    if (maxsend<1)
	maxsend = 1;

    for (i=0 ; i<doomcom->numplayers ; i++)
	playeringame[i] = true;
    for (i=0 ; i<doomcom->numnodes ; i++)
	nodeingame[i] = true;

    fprintf (logfile, "player %i of %i (%i nodes)\n",
	    consoleplayer+1, doomcom->numplayers, doomcom->numnodes);

}



/* D_QuitNetGame*/
/* Called before quitting to leave a net game*/
/* without hanging the other players*/

void D_QuitNetGame (void)
{
    int             i, j;

    if (debugfile)
	fclose (debugfile);

    if (!netgame || !usergame || consoleplayer == -1 || demoplayback)
	return;

    /* send a bunch of packets for security*/
    netbuffer->player = consoleplayer;
    netbuffer->numtics = 0;
    for (i=0 ; i<4 ; i++)
    {
	for (j=1 ; j<doomcom->numnodes ; j++)
	    if (nodeingame[j])
		HSendPacket (j, NCMD_EXIT);
	I_WaitVBL (1);
    }
    I_ShutdownNetwork();
}




/* TryRunTics*/

/*static int	frametics[4];*/
static int	frameon;
static int	frameskip[4];
static int	oldnettics;

void TryRunTics (void)
{
    int		i;
    int		lowtic;
    int		entertic;
    static int	oldentertics;
    int		realtics;
    int		availabletics;
    int		counts;
    int		numplaying;

    /* get real tics		*/
    entertic = I_GetTime ()/ticdup;
    realtics = entertic - oldentertics;
    oldentertics = entertic;

    /* get available tics*/
    NetUpdate ();

    lowtic = MAXINT;
    numplaying = 0;
    for (i=0 ; i<doomcom->numnodes ; i++)
    {
	if (nodeingame[i])
	{
	    numplaying++;
	    if (nettics[i] < lowtic)
		lowtic = nettics[i];
	}
    }
    availabletics = lowtic - gametic/ticdup;

    /* decide how many tics to run*/
    if (realtics < availabletics-1)
	counts = realtics+1;
    else if (realtics < availabletics)
	counts = realtics;
    else
	counts = availabletics;

    if (counts < 1)
	counts = 1;

    frameon++;

    if (debugfile)
	fprintf (debugfile,
		 "=======real: %i  avail: %i  game: %i\n",
		 realtics, availabletics,counts);

    if (!demoplayback)
    {
	/* ideally nettics[0] should be 1 - 3 tics above lowtic*/
	/* if we are consistantly slower, speed up time*/
	for (i=0 ; i<MAXPLAYERS ; i++)
	    if (playeringame[i])
		break;
	if (consoleplayer == i)
	{
	    /* the key player does not adapt*/
	}
	else
	{
	    if (nettics[0] <= nettics[nodeforplayer[i]])
	    {
		gametime--;
		/* printf ("-");*/
	    }
	    frameskip[frameon&3] = (oldnettics > nettics[nodeforplayer[i]]);
	    oldnettics = nettics[0];
	    if (frameskip[0] && frameskip[1] && frameskip[2] && frameskip[3])
	    {
		skiptics = 1;
		/* printf ("+");*/
	    }
	}
    }/* demoplayback*/

    /* wait for new tics if needed*/
    while (lowtic < gametic/ticdup + counts)
    {
	NetUpdate ();
	lowtic = MAXINT;

	for (i=0 ; i<doomcom->numnodes ; i++)
	    if (nodeingame[i] && nettics[i] < lowtic)
		lowtic = nettics[i];

	if (lowtic < gametic/ticdup)
	    I_Error ("TryRunTics: lowtic < gametic");

	/* don't stay in here forever -- give the menu a chance to work*/
	if (I_GetTime ()/ticdup - entertic >= 20)
	{
	    M_Ticker ();
	    return;
	}
    }

    /* run the count * ticdup dics*/
    while (counts--)
    {
	for (i=0 ; i<ticdup ; i++)
	{
	    if (gametic/ticdup > lowtic)
		I_Error ("gametic>lowtic");
	    if (advancedemo)
		D_DoAdvanceDemo ();
	    M_Ticker ();
	    G_Ticker ();
	    gametic++;

	    /* modify command for duplicated tics*/
	    if (i != ticdup-1)
	    {
		ticcmd_t	*cmd;
		int			buf;
		int			j;

		buf = (gametic/ticdup)%BACKUPTICS;
		for (j=0 ; j<MAXPLAYERS ; j++)
		{
		    cmd = &netcmds[j][buf];
		    cmd->chatchar = 0;
		    if (cmd->buttons & BT_SPECIAL)
			cmd->buttons = 0;
		}
	    }
	}
	NetUpdate ();	/* check for new console commands*/
    }
}
