ref: 73fd9adc1e23f711d249d4adc2389711304bd027
dir: /src/net_client.c/
// Emacs style mode select   -*- C++ -*- 
//-----------------------------------------------------------------------------
//
// Copyright(C) 2005 Simon Howard
//
// 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.
//
// Network client code
//
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "config.h"
#include "doomtype.h"
#include "deh_main.h"
#include "deh_str.h"
#include "i_system.h"
#include "i_timer.h"
#include "m_argv.h"
#include "m_fixed.h"
#include "m_config.h"
#include "net_client.h"
#include "net_common.h"
#include "net_defs.h"
#include "net_gui.h"
#include "net_io.h"
#include "net_packet.h"
#include "net_server.h"
#include "net_structrw.h"
#include "w_checksum.h"
#include "w_wad.h"
extern void D_ReceiveTic(ticcmd_t *ticcmds, boolean *playeringame);
typedef enum
{
    // waiting for the game to start
    CLIENT_STATE_WAITING_START,
    // in game
    CLIENT_STATE_IN_GAME,
} net_clientstate_t;
// Type of structure used in the receive window
typedef struct
{
    // Whether this tic has been received yet
    boolean active;
    // Last time we sent a resend request for this tic
    unsigned int resend_time;
    // Tic data from server 
    net_full_ticcmd_t cmd;
    
} net_server_recv_t;
// Type of structure used in the send window
typedef struct
{
    // Whether this slot is active yet
    boolean active;
    // The tic number
    unsigned int seq;
    // Time the command was generated
    unsigned int time;
    // Ticcmd diff
    net_ticdiff_t cmd;
} net_server_send_t;
extern fixed_t offsetms;
static net_connection_t client_connection;
static net_clientstate_t client_state;
static net_addr_t *server_addr;
static net_context_t *client_context;
// game settings, as received from the server when the game started
static net_gamesettings_t settings;
// true if the client code is in use
boolean net_client_connected;
// true if we have received waiting data from the server
boolean net_client_received_wait_data;
net_waitdata_t net_client_wait_data;
// Waiting for the game to start?
boolean net_waiting_for_start = false;
// Name that we send to the server
char *net_player_name = NULL;
// Connected but not participating in the game (observer)
boolean drone = false;
// The last ticcmd constructed
static ticcmd_t last_ticcmd;
// Buffer of ticcmd diffs being sent to the server
static net_server_send_t send_queue[BACKUPTICS];
// Receive window
static ticcmd_t recvwindow_cmd_base[NET_MAXPLAYERS];
static int recvwindow_start;
static net_server_recv_t recvwindow[BACKUPTICS];
// Whether we need to send an acknowledgement and
// when gamedata was last received.
static boolean need_to_acknowledge;
static unsigned int gamedata_recv_time;
// Hash checksums of our wad directory and dehacked data.
sha1_digest_t net_local_wad_sha1sum;
sha1_digest_t net_local_deh_sha1sum;
// Are we playing with the freedoom IWAD?
unsigned int net_local_is_freedoom;
// Average time between sending our ticcmd and receiving from the server
static fixed_t average_latency;
#define NET_CL_ExpandTicNum(b) NET_ExpandTicNum(recvwindow_start, (b))
// Called when we become disconnected from the server
static void NET_CL_Disconnected(void)
{
    D_ReceiveTic(NULL, NULL);
}
// Expand a net_full_ticcmd_t, applying the diffs in cmd->cmds as
// patches against recvwindow_cmd_base.  Place the results into
// the d_net.c structures (netcmds/nettics) and save the new ticcmd
// back into recvwindow_cmd_base.
static void NET_CL_ExpandFullTiccmd(net_full_ticcmd_t *cmd, unsigned int seq,
                                    ticcmd_t *ticcmds)
{
    int latency;
    fixed_t adjustment;
    int i;
    // Update average_latency
    if (seq == send_queue[seq % BACKUPTICS].seq)
    {
        latency = I_GetTimeMS() - send_queue[seq % BACKUPTICS].time;
    }
    else if (seq > send_queue[seq % BACKUPTICS].seq)
    {
        // We have received the ticcmd from the server before we have
        // even sent ours
        latency = 0;
    }
    else
    {
        latency = -1;
    }
    if (latency >= 0)
    {
        if (seq <= 20)
        {
            average_latency = latency * FRACUNIT;
        }
        else
        {
            // Low level filter
            average_latency = (fixed_t)((average_latency * 0.9)
                            + (latency * FRACUNIT * 0.1));
        }
    }
    //printf("latency: %i\tremote:%i\n", average_latency / FRACUNIT, 
    //                                   cmd->latency);
    // Possibly adjust offsetms in d_net.c, try to make players all have
    // the same lag.  Don't adjust in the first few tics of play, as 
    // we don't have an accurate value for average_latency yet.
    if (seq > TICRATE)
    {
        adjustment = (cmd->latency * FRACUNIT) - average_latency;
        // Only adjust very slightly; the cumulative effect over 
        // multiple tics will sort it out.
        adjustment = adjustment / 100;
        offsetms += adjustment;
    }
    // Expand tic diffs for all players
    
    for (i=0; i<NET_MAXPLAYERS; ++i)
    {
        if (i == settings.consoleplayer && !drone)
        {
            continue;
        }
        
        if (cmd->playeringame[i])
        {
            net_ticdiff_t *diff;
            diff = &cmd->cmds[i];
            // Use the ticcmd diff to patch the previous ticcmd to
            // the new ticcmd
            NET_TiccmdPatch(&recvwindow_cmd_base[i], diff, &ticcmds[i]);
            // Store a copy for next time
            recvwindow_cmd_base[i] = ticcmds[i];
        }
    }
}
// Advance the receive window
static void NET_CL_AdvanceWindow(void)
{
    ticcmd_t ticcmds[NET_MAXPLAYERS];
    while (recvwindow[0].active)
    {
        // Expand tic diff data into d_net.c structures
        NET_CL_ExpandFullTiccmd(&recvwindow[0].cmd, recvwindow_start,
                                ticcmds);
        D_ReceiveTic(ticcmds, recvwindow[0].cmd.playeringame);
        // Advance the window
        memcpy(recvwindow, recvwindow + 1, 
               sizeof(net_server_recv_t) * (BACKUPTICS - 1));
        memset(&recvwindow[BACKUPTICS-1], 0, sizeof(net_server_recv_t));
        ++recvwindow_start;
        //printf("CL: advanced to %i\n", recvwindow_start);
    }
}
// Shut down the client code, etc.  Invoked after a disconnect.
static void NET_CL_Shutdown(void)
{
    if (net_client_connected)
    {
        net_client_connected = false;
        NET_FreeAddress(server_addr);
        // Shut down network module, etc.  To do.
    }
}
void NET_CL_StartGame(net_gamesettings_t *settings)
{
    net_packet_t *packet;
    // Start from a ticcmd of all zeros
    memset(&last_ticcmd, 0, sizeof(ticcmd_t));
    
    // Send packet
    packet = NET_Conn_NewReliable(&client_connection, 
                                  NET_PACKET_TYPE_GAMESTART);
    NET_WriteSettings(packet, settings);
}
static void NET_CL_SendGameDataACK(void)
{
    net_packet_t *packet;
    packet = NET_NewPacket(10);
    NET_WriteInt16(packet, NET_PACKET_TYPE_GAMEDATA_ACK);
    NET_WriteInt8(packet, recvwindow_start & 0xff);
    NET_Conn_SendPacket(&client_connection, packet);
    NET_FreePacket(packet);
    need_to_acknowledge = false;
}
static void NET_CL_SendTics(int start, int end)
{
    net_packet_t *packet;
    int i;
    if (!net_client_connected)
    {
        // Disconnected from server
        return;
    }
    if (start < 0)
        start = 0;
    
    // Build a new packet to send to the server
    packet = NET_NewPacket(512);
    NET_WriteInt16(packet, NET_PACKET_TYPE_GAMEDATA);
    // Write the start tic and number of tics.  Send only the low byte
    // of start - it can be inferred by the server.
    NET_WriteInt8(packet, recvwindow_start & 0xff);
    NET_WriteInt8(packet, start & 0xff);
    NET_WriteInt8(packet, end - start + 1);
    // Add the tics.
    for (i=start; i<=end; ++i)
    {
        net_server_send_t *sendobj;
        sendobj = &send_queue[i % BACKUPTICS];
        NET_WriteInt16(packet, average_latency / FRACUNIT);
        NET_WriteTiccmdDiff(packet, &sendobj->cmd, settings.lowres_turn);
    }
    
    // Send the packet
    NET_Conn_SendPacket(&client_connection, packet);
    
    // All done!
    NET_FreePacket(packet);
    // Acknowledgement has been sent as part of the packet
    need_to_acknowledge = false;
}
// Add a new ticcmd to the send queue
void NET_CL_SendTiccmd(ticcmd_t *ticcmd, int maketic)
{
    net_ticdiff_t diff;
    net_server_send_t *sendobj;
    int starttic, endtic;
    
    // Calculate the difference to the last ticcmd
    NET_TiccmdDiff(&last_ticcmd, ticcmd, &diff);
    
    // Store in the send queue
    sendobj = &send_queue[maketic % BACKUPTICS];
    sendobj->active = true;
    sendobj->seq = maketic;
    sendobj->time = I_GetTimeMS();
    sendobj->cmd = diff;
    last_ticcmd = *ticcmd;
    // Send to server.
    starttic = maketic - settings.extratics;
    endtic = maketic;
    if (starttic < 0)
        starttic = 0;
    
    NET_CL_SendTics(starttic, endtic);
}
// data received while we are waiting for the game to start
static void NET_CL_ParseWaitingData(net_packet_t *packet)
{
    net_waitdata_t wait_data;
    if (!NET_ReadWaitData(packet, &wait_data))
    {
        // Invalid packet?
        return;
    }
    if (wait_data.num_players > wait_data.max_players
     || wait_data.max_players > NET_MAXPLAYERS)
    {
        // insane data
        return;
    }
    if ((wait_data.consoleplayer >= 0 && drone)
     || (wait_data.consoleplayer < 0 && !drone)
     || (wait_data.consoleplayer >= wait_data.num_players))
    {
        // Invalid player number
        return;
    }
    memcpy(&net_client_wait_data, &wait_data, sizeof(net_waitdata_t));
    net_client_received_wait_data = true;
}
static void NET_CL_ParseGameStart(net_packet_t *packet)
{
    if (!NET_ReadSettings(packet, &settings))
    {
        return;
    }
    if (client_state != CLIENT_STATE_WAITING_START)
    {
        return;
    }
    if (settings.num_players > NET_MAXPLAYERS
     || settings.consoleplayer >= (signed int) settings.num_players)
    {
        // insane values
        return;
    }
    if ((drone && settings.consoleplayer >= 0)
     || (!drone && settings.consoleplayer < 0))
    {
        // Invalid player number: must be positive for real players,
        // negative for drones
        return;
    }
    client_state = CLIENT_STATE_IN_GAME;
    // Clear the receive window
    memset(recvwindow, 0, sizeof(recvwindow));
    recvwindow_start = 0;
    memset(&recvwindow_cmd_base, 0, sizeof(recvwindow_cmd_base));
    // Clear the send queue
    memset(&send_queue, 0x00, sizeof(send_queue));
}
static void NET_CL_SendResendRequest(int start, int end)
{
    net_packet_t *packet;
    unsigned int nowtime;
    int i;
    //printf("CL: Send resend %i-%i\n", start, end);
    
    packet = NET_NewPacket(64);
    NET_WriteInt16(packet, NET_PACKET_TYPE_GAMEDATA_RESEND);
    NET_WriteInt32(packet, start);
    NET_WriteInt8(packet, end - start + 1);
    NET_Conn_SendPacket(&client_connection, packet);
    NET_FreePacket(packet);
    nowtime = I_GetTimeMS();
    // Save the time we sent the resend request
    for (i=start; i<=end; ++i)
    {
        int index;
        index = i - recvwindow_start;
        if (index < 0 || index >= BACKUPTICS)
            continue;
        recvwindow[index].resend_time = nowtime;
    }
}
// Check for expired resend requests
static void NET_CL_CheckResends(void)
{
    int i;
    int resend_start, resend_end;
    unsigned int nowtime;
    nowtime = I_GetTimeMS();
    resend_start = -1;
    resend_end = -1;
    for (i=0; i<BACKUPTICS; ++i)
    {
        net_server_recv_t *recvobj;
        boolean need_resend;
        recvobj = &recvwindow[i];
        // if need_resend is true, this tic needs another retransmit
        // request (300ms timeout)
        need_resend = !recvobj->active
                   && recvobj->resend_time != 0
                   && nowtime > recvobj->resend_time + 300;
        if (need_resend)
        {
            // Start a new run of resend tics?
 
            if (resend_start < 0)
            {
                resend_start = i;
            }
            
            resend_end = i;
        }
        else
        {
            if (resend_start >= 0)
            {
                // End of a run of resend tics
                //printf("CL: resend request timed out: %i-%i\n", resend_start, resend_end);
                NET_CL_SendResendRequest(recvwindow_start + resend_start,
                                         recvwindow_start + resend_end);
                resend_start = -1;
            }
        }
    }
    if (resend_start >= 0)
    {
        //printf("CL: resend request timed out: %i-%i\n", resend_start, resend_end);
        NET_CL_SendResendRequest(recvwindow_start + resend_start,
                                 recvwindow_start + resend_end);
    }
    // We have received some data from the server and not acknowledged
    // it yet.  Normally this gets acknowledged when we send our game
    // data, but if the client is a drone we need to do this.
    if (need_to_acknowledge && nowtime - gamedata_recv_time > 200)
    {
        NET_CL_SendGameDataACK();
    }
}
// Parsing of NET_PACKET_TYPE_GAMEDATA packets
// (packets containing the actual ticcmd data)
static void NET_CL_ParseGameData(net_packet_t *packet)
{
    net_server_recv_t *recvobj;
    unsigned int seq, num_tics;
    unsigned int nowtime;
    int resend_start, resend_end;
    size_t i;
    int index;
    
    // Read header
    
    if (!NET_ReadInt8(packet, &seq)
     || !NET_ReadInt8(packet, &num_tics))
    {
        return;
    }
    nowtime = I_GetTimeMS();
    // Whatever happens, we now need to send an acknowledgement of our
    // current receive point.
    if (!need_to_acknowledge)
    {
        need_to_acknowledge = true;
        gamedata_recv_time = nowtime;
    }
    // Expand byte value into the full tic number
    seq = NET_CL_ExpandTicNum(seq);
    for (i=0; i<num_tics; ++i)
    {
        net_full_ticcmd_t cmd;
        index = seq - recvwindow_start + i;
        if (!NET_ReadFullTiccmd(packet, &cmd, settings.lowres_turn))
        {
            return;
        }
        if (index < 0 || index >= BACKUPTICS)
        {
            // Out of range of the recv window
            continue;
        }
        // Store in the receive window
        
        recvobj = &recvwindow[index];
        recvobj->active = true;
        recvobj->cmd = cmd;
    }
    // Has this been received out of sequence, ie. have we not received
    // all tics before the first tic in this packet?  If so, send a 
    // resend request.
    //printf("CL: %p: %i\n", client, seq);
    resend_end = seq - recvwindow_start;
    if (resend_end <= 0)
        return;
    if (resend_end >= BACKUPTICS)
        resend_end = BACKUPTICS - 1;
    index = resend_end - 1;
    resend_start = resend_end;
    
    while (index >= 0)
    {
        recvobj = &recvwindow[index];
        if (recvobj->active)
        {
            // ended our run of unreceived tics
            break;
        }
        if (recvobj->resend_time != 0)
        {
            // Already sent a resend request for this tic
            break;
        }
        resend_start = index;
        --index;
    }
    // Possibly send a resend request
    if (resend_start < resend_end)
    {
        NET_CL_SendResendRequest(recvwindow_start + resend_start, 
                                 recvwindow_start + resend_end - 1);
    }
}
// Parse a resend request from the server due to a dropped packet
static void NET_CL_ParseResendRequest(net_packet_t *packet)
{
    static unsigned int start;
    static unsigned int end;
    static unsigned int num_tics;
    if (drone)
    {
        // Drones don't send gamedata.
        return;
    }
    if (!NET_ReadInt32(packet, &start)
     || !NET_ReadInt8(packet, &num_tics))
    {
        return;
    }
    end = start + num_tics - 1;
    //printf("requested resend %i-%i .. ", start, end);
    // Check we have the tics being requested.  If not, reduce the 
    // window of tics to only what we have.
    while (start <= end
        && (!send_queue[start % BACKUPTICS].active
         || send_queue[start % BACKUPTICS].seq != start))
    {
        ++start;
    }
     
    while (start <= end
        && (!send_queue[end % BACKUPTICS].active
         || send_queue[end % BACKUPTICS].seq != end))
    {
        --end;
    }
    //printf("%i-%i\n", start, end);
    // Resend those tics
    if (start <= end)
    {
        //printf("CL: resend %i-%i\n", start, start+num_tics-1);
        NET_CL_SendTics(start, end);
    }
}
// Console message that the server wants the client to print
static void NET_CL_ParseConsoleMessage(net_packet_t *packet)
{
    char *msg;
    msg = NET_ReadString(packet);
    if (msg == NULL)
    {
        return;
    }
    printf("Message from server: ");
    NET_SafePuts(msg);
}
// parse a received packet
static void NET_CL_ParsePacket(net_packet_t *packet)
{
    unsigned int packet_type;
    if (!NET_ReadInt16(packet, &packet_type))
    {
        return;
    }
    if (NET_Conn_Packet(&client_connection, packet, &packet_type))
    {
        // Packet eaten by the common connection code
    }
    else
    {
        switch (packet_type)
        {
            case NET_PACKET_TYPE_WAITING_DATA:
                NET_CL_ParseWaitingData(packet);
                break;
            case NET_PACKET_TYPE_GAMESTART:
                NET_CL_ParseGameStart(packet);
                break;
            case NET_PACKET_TYPE_GAMEDATA:
                NET_CL_ParseGameData(packet);
                break;
            case NET_PACKET_TYPE_GAMEDATA_RESEND:
                NET_CL_ParseResendRequest(packet);
                break;
            case NET_PACKET_TYPE_CONSOLE_MESSAGE:
                NET_CL_ParseConsoleMessage(packet);
                break;
            default:
                break;
        }
    }
}
// "Run" the client code: check for new packets, send packets as
// needed
void NET_CL_Run(void)
{
    net_addr_t *addr;
    net_packet_t *packet;
    
    if (!net_client_connected)
    {
        return;
    }
    
    while (NET_RecvPacket(client_context, &addr, &packet))
    {
        // only accept packets from the server
        if (addr == server_addr)
        {
            NET_CL_ParsePacket(packet);
        }
        else
        {
            NET_FreeAddress(addr);
        }
        NET_FreePacket(packet);
    }
    // Run the common connection code to send any packets as needed
    NET_Conn_Run(&client_connection);
    if (client_connection.state == NET_CONN_STATE_DISCONNECTED
     || client_connection.state == NET_CONN_STATE_DISCONNECTED_SLEEP)
    {
        NET_CL_Disconnected();
    
        NET_CL_Shutdown();
    }
    
    net_waiting_for_start = client_connection.state == NET_CONN_STATE_CONNECTED
                         && client_state == CLIENT_STATE_WAITING_START;
    if (client_state == CLIENT_STATE_IN_GAME)
    {
        // Possibly advance the receive window
        NET_CL_AdvanceWindow();
        // Check if our resend requests have timed out
        NET_CL_CheckResends();
    }
}
static void NET_CL_SendSYN(net_connect_data_t *data)
{
    net_packet_t *packet;
    packet = NET_NewPacket(10);
    NET_WriteInt16(packet, NET_PACKET_TYPE_SYN);
    NET_WriteInt32(packet, NET_MAGIC_NUMBER);
    NET_WriteString(packet, PACKAGE_STRING);
    NET_WriteConnectData(packet, data);
    NET_WriteString(packet, net_player_name);
    NET_Conn_SendPacket(&client_connection, packet);
    NET_FreePacket(packet);
}
// connect to a server
boolean NET_CL_Connect(net_addr_t *addr, net_connect_data_t *data)
{
    int start_time;
    int last_send_time;
    server_addr = addr;
    memcpy(net_local_wad_sha1sum, data->wad_sha1sum, sizeof(sha1_digest_t));
    memcpy(net_local_deh_sha1sum, data->deh_sha1sum, sizeof(sha1_digest_t));
    net_local_is_freedoom = data->is_freedoom;
    // create a new network I/O context and add just the
    // necessary module
    client_context = NET_NewContext();
    // initialize module for client mode
    if (!addr->module->InitClient())
    {
        return false;
    }
    NET_AddModule(client_context, addr->module);
    net_client_connected = true;
    net_client_received_wait_data = false;
    // Initialize connection
    NET_Conn_InitClient(&client_connection, addr);
    // try to connect
 
    start_time = I_GetTimeMS();
    last_send_time = -1;
    while (client_connection.state == NET_CONN_STATE_CONNECTING)
    {
        int nowtime = I_GetTimeMS();
        // Send a SYN packet every second.
        if (nowtime - last_send_time > 1000 || last_send_time < 0)
        {
            NET_CL_SendSYN(data);
            last_send_time = nowtime;
        }
 
        // time out after 5 seconds 
        if (nowtime - start_time > 5000)
        {
            break;
        }
        // run client code
        NET_CL_Run();
        // run the server, just incase we are doing a loopback
        // connect
        NET_SV_Run();
        // Don't hog the CPU
        I_Sleep(1);
    }
    if (client_connection.state == NET_CONN_STATE_CONNECTED)
    {
        // connected ok!
        client_state = CLIENT_STATE_WAITING_START;
        drone = data->drone;
        return true;
    }
    else
    {
        // failed to connect
        NET_CL_Shutdown();
        return false;
    }
}
// read game settings received from server
boolean NET_CL_GetSettings(net_gamesettings_t *_settings)
{
    if (client_state != CLIENT_STATE_IN_GAME)
    {
        return false;
    }
    memcpy(_settings, &settings, sizeof(net_gamesettings_t));
    return true;
}
// disconnect from the server
void NET_CL_Disconnect(void)
{
    int start_time;
    if (!net_client_connected)
    {
        return;
    }
    
    NET_Conn_Disconnect(&client_connection);
    start_time = I_GetTimeMS();
    while (client_connection.state != NET_CONN_STATE_DISCONNECTED
        && client_connection.state != NET_CONN_STATE_DISCONNECTED_SLEEP)
    {
        if (I_GetTimeMS() - start_time > 5000)
        {
            // time out after 5 seconds
            
            client_state = NET_CONN_STATE_DISCONNECTED;
            fprintf(stderr, "NET_CL_Disconnect: Timeout while disconnecting from server\n");
            break;
        }
        NET_CL_Run();
        NET_SV_Run();
        I_Sleep(1);
    }
    // Finished sending disconnect packets, etc.
    NET_CL_Shutdown();
}
void NET_CL_Init(void)
{
    // Try to set from the USER and USERNAME environment variables
    // Otherwise, fallback to "Player"
    if (net_player_name == NULL) 
        net_player_name = getenv("USER");
    if (net_player_name == NULL)
        net_player_name = getenv("USERNAME");
    // On Windows, environment variables are in OEM codepage
    // encoding, so convert to UTF8:
#ifdef _WIN32
    if (net_player_name != NULL)
    {
        net_player_name = M_OEMToUTF8(net_player_name);
    }
#endif
    if (net_player_name == NULL)
        net_player_name = "Player";
}
void NET_Init(void)
{
    NET_CL_Init();
}
void NET_BindVariables(void)
{
    M_BindVariable("player_name", &net_player_name);
}