Netgame - State Correction

Overview Game state variables Global variables for state correction How a client gets a new state from the server How a client applies the new state How the server sends a new state dif to a client Packets used for state correction

Overview The state correction method perodically sends the entire game state from the server to the client. This fixes any drift or loss of sync that may have occured with the game move sync method. The entire game state is over 100,000 bytes. To pass this large chunk of data across the network, two compression methods are used. The first method is a state and dif scheme, to detect only the data that has changed from the last state. The second method uses the zlib compression library to compress the dif from the first method. Using zlib only resulted in 5% to 10% compression. Using both gets 1% - 2%. WIth 1024 byte packets, I can usually send the entire game state in 1 or 2 packets. When a state dif is made without a previous state to base it on it is larger and needs 4-8 packets. This happens for the initial state, or rarely if the base state is lost somehow.
Game state variables The game state variables take 104640 bytes.

    // 40,000 bytes for the 100 x 100 block level
    extern int l[100][100];

    // 1440 bytes for 8 players
    extern struct player players[NUM_PLAYERS];

    // 4000 bytes for lifts:
    extern struct lift lifts[NUM_LIFTS];

    // 40,000 bytes for 500 items
    extern int item[500][16];      // item ints
    extern al_fixed itemf[500][4]; // item fixeds

    // 19,200 bytes for 100 enemies
    extern int Ei[100][32];        // enemy ints
    extern al_fixed Efi[100][16];  // enemy fixeds

    // 104,640 bytes total

To work with this huge chuck of data as a whole, I move it to a char array like this:

    char tmp[104640];
    game_vars_to_state(tmp);

    void game_vars_to_state(char * b)
    {
       int size = 0, offset = 0;
       offset += size; size = sizeof(players); memcpy(b+offset, players, size);
       offset += size; size = sizeof(Ei);      memcpy(b+offset, Ei,      size);
       offset += size; size = sizeof(Efi);     memcpy(b+offset, Efi,     size);
       offset += size; size = sizeof(item);    memcpy(b+offset, item,    size);
       offset += size; size = sizeof(itemf);   memcpy(b+offset, itemf,   size);
       offset += size; size = sizeof(lifts);   memcpy(b+offset, lifts,   size);
       offset += size; size = sizeof(l);       memcpy(b+offset, l,       size);
    }

This is how the char array is put back into the game variables:

    void state_to_game_vars(char * b)
    {
       int size = 0, offset = 0;
       size = sizeof(players); memcpy(players, b+offset, size); offset += size;
       size = sizeof(Ei);      memcpy(Ei,      b+offset, size); offset += size;
       size = sizeof(Efi);     memcpy(Efi,     b+offset, size); offset += size;
       size = sizeof(item);    memcpy(item,    b+offset, size); offset += size;
       size = sizeof(itemf);   memcpy(itemf,   b+offset, size); offset += size;
       size = sizeof(lifts);   memcpy(lifts,   b+offset, size); offset += size;
       size = sizeof(l);       memcpy(l,       b+offset, size); offset += size;
    }

I call these large char arrays 'states'. The client uses 3 and the server uses 16 (2 for each player).
Global variables for state correction These are the global data stuctures used for the game state correction algorithm:

    #define STATE_SIZE 104640

    // server's copies of client states
    extern char srv_client_state[8][2][STATE_SIZE];
    extern int srv_client_state_frame_num[8][2];

    // local client's states
    extern char client_state_buffer[STATE_SIZE];  // buffer for building compressed dif from packet pieces
    extern int  client_state_buffer_pieces[16];   // to mark packet pieces as received
    extern char client_state_base[STATE_SIZE];    // last ack state
    extern int  client_state_base_frame_num;      // last ack state frame_num
    extern char client_state_dif[STATE_SIZE];     // uncompressed dif
    extern int  client_state_dif_src;             // uncompressed dif src frame_num
    extern int  client_state_dif_dst;             // uncompressed dif dst frame_num

How a client gets a new state from the server A client receives 'stdf' packets from the server. Each packet contains pieces of a compressed dif state. These packets have up to 1000 bytes of data each and are put into 'client_state_buffer' at the appropriate offset. When all the pieces have been received, 'client_state_buffer' is uncompressed into 'client_state_dif'. The source and destination frame of 'client_state_dif' are updated to mark it as valid.

    int process_stdf_packet(void)
    {
       int src = PacketGet4ByteInt();
       int dst = PacketGet4ByteInt();
       int seq = PacketGet1ByteInt();
       int max_seq = PacketGet1ByteInt();
       int sb = PacketGet4ByteInt();
       int sz = PacketGet4ByteInt();

       memcpy(client_state_buffer + sb, packetbuffer+22, sz);  // put the piece of data in the buffer
       client_state_buffer_pieces[seq] = dst; // mark it with destination frame_num

       int complete = 1; // did we just get the last packet? (yes by default)
       for (int i=0; i< max_seq; i++)
          if (client_state_buffer_pieces[i] != dst) complete = 0; // no, if any piece not at latest frame_num

       if (complete)
       {
          // decompress client_state_buffer to client_state_dif
          uncompress((Bytef*)client_state_dif, sizeof(client_state_dif), (Bytef*)client_state_buffer, sizeof(client_state_buffer));

          // mark dif data with new src and dst
          client_state_dif_src = src;
          client_state_dif_dst = dst;
       }
    }

How a client applies the new state Every frame the client checks to see if it has a dif that matches both these conditions: - destination frame matches current frame - source frame matches the client's stored base state If these conditions are met: - the dif is applied to the base state - the base state is used to overwrite the game variables - the base state's frame number is updated to the current frame - an acknowledge packet is sent to the server

    void client_apply_diff()
    {
       if (frame_num == client_state_dif_dst)                                  // current frame_num is dif destination
          if (client_state_base_frame_num == client_state_dif_src)             // stored base state matches dif source
          {
             apply_state_dif(client_state_base, client_state_dif, STATE_SIZE); // apply dif to base state
             state_to_game_vars(client_state_base);                            // copy modified base state to game_vars
             client_state_base_frame_num = frame_num;                          // update client base frame_num

             Packet("stak"); // send acknowledge to server with last state sucessfully applied
             PacketPut1ByteInt(p);
             PacketPut1ByteInt(dif_corr);
             PacketPut4ByteInt(frame_num);
             ClientSend(packetbuffer, packetsize);
          }
    }

How the server sends a new state dif to a client The server keeps 2 states for each client. The 1st state:

    srv_client_state[p][0]
    srv_client_state_frame_num[p][0]

- is the base (or last acknowledged) state for that client - the server uses it to make new difs based on that state - the client also keeps a copy of it and uses it to apply the difs it gets from the server The 2nd state:

    srv_client_state[p][1]
    srv_client_state_frame_num[p][1]

- is used as the destination state when making a dif for a client - is kept by the server until acknowledged by the client, or a new dif is made Here is how the server makes a dif to send to a client: - the current server game state is copied to the 2nd state

    // get current state
    game_vars_to_state(srv_client_state[p][1]);

    // set dif dest to current frame_num
    srv_client_state_frame_num[p][1] = frame_num;

- a dif is made by subtracting the 2nd state from the 1st state

    // make a new dif from base and current
    get_state_dif(srv_client_state[p][0], srv_client_state[p][1], dif, STATE_SIZE);

    void get_state_dif(char *a, char *b, char *c, int size)
    {
       for (int i=0; i < size; i++)
          c[i] = a[i] - b[i];
    }

- the dif is compressed

    // compress dif to cmp
    uLongf destLen= sizeof(cmp);
    compress2((Bytef*)cmp, (uLongf*)&destLen, (Bytef*)dif, sizeof(dif), zlib_cmp);
    int cp = destLen;

- the compressed dif is broken into 1000 byte pieces and sent with 'stdf' packets to the client - each 'stdf' packet has a header with all the information needed to re-assemble, decompress and apply

    // break compressed dif into smaller pieces
    int start_byte = 0;
    int num_packets = (cmp_size / 1000) + 1;
    for (int packet_num=0; packet_num < num_packets; packet_num++)
    {
       int packet_data_size = 1000; // default size
       if (start_byte + packet_data_size > cmp_size) packet_data_size = cmp_size - start_byte; // last piece is smaller

       Packet("stdf");
       PacketPut4ByteInt(srv_client_state_frame_num[p][0]); // src frame_num
       PacketPut4ByteInt(srv_client_state_frame_num[p][1]); // dst frame_num
       PacketPut1ByteInt(packet_num);
       PacketPut1ByteInt(num_packets);
       PacketPut4ByteInt(start_byte);
       PacketPut4ByteInt(packet_data_size);
       memcpy(packetbuffer+packetsize, cmp+start_byte, packet_data_size);
       packetsize += packet_data_size;
       ServerSendTo(packetbuffer, packetsize, players1[p].who, p);

       start_byte+=1000;
    }

After the 'stdf' packets are sent, the server expects an acknowledge packet 'stak' from the client. When an acknowledgement is received, the 2nd state is copied to the 1st state and becomes the new base. If no acknowledgement is received, the next dif will use the same base as the last one. (but with a new destination)

    if(PacketRead("stak"))
    {
       int ack_pc = PacketGet4ByteInt();  // client acknowledged up to here
       if (ack_pc == srv_client_state_frame_num[p][1]) // check to make sure we have a copy of acknowledged state
       {
          // acknowledged state is out new base state
          memcpy(srv_client_state[p][0], srv_client_state[p][1], STATE_SIZE); // copy 1 to 0
    	  srv_client_state_frame_num[p][0] = ack_pc; // new frame_num
       }
       else // we don't have a copy of acknowledged state !!!  // set new base failed!
       {
          // reset base to zero
          memset(srv_client_state[p][0], 0, STATE_SIZE);

          // set base frame_num to 0
          srv_client_state_frame_num[p][0] = 0;
       }
    }

Resetting base state Resetting the base to zero means setting all 104,640 bytes to zero, and setting the frame_num to 0; On the server, a client's base state is used to make new difs to send to that client. The client applies difs from the server to its local copy of the same base state, and acknowledges them. If the server does not have a copy of the state acknowledged by the client, it resets the client's base to zero. The next dif sent will be based on state 0 and will be considerably larger than a regular dif. If a client receives a dif that has base state zero, it resets its base to zero. At the start of a level, the first dif the server sends will be from base zero. After that it should be quite rare for the base to be reset. It can happpen if difs are being sent with too short of a delay. (stdf_freq set too low) A client gets a dif, applies it, updates its base, and sends an acknowledge. Before server gets the acknowledge, the server sends a new dif, overwriting the dest used to make the last one. Then when the server does get the acknowledge, it no longer has the neeeded copy of the client's new base.
Packets used for state correction Packet: 'stdf' description: 'state dif' direction: server to client - 4 bytes (source frame_num) - 4 bytes (destination frame_num) - 1 byte (packet sequence num) - 1 byte (packet sequence total) - 4 bytes (data start byte) - 4 bytes (data size) - up to 1000 bytes of compressed dif data
Packet: 'stak' description: 'state dif acknowledge' direction: client to server - 1 byte (player) - 1 byte (diff corr) - 4 bytes (client frame_num)