Purple Martians
Technical Code Descriptions
Netgame - Client State
Overview
How a client gets a new state from the server
How a client applies the new state
The entire function 'client_apply_dif'
Overview
The server consolidates input from all clients and syncs the official server state back to clients.
Clients run independently in between receiving states from the server.
During that time, the client's control changes are applied locally and sent to the server.
When a client receives a new state from the server, it overwrites its local game state.
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 decompressed into 'client_state_dif'.
The source and destination frame of 'client_state_dif' are updated to mark it as valid.
void mwNetgame::client_process_stdf_packet(int i)
{
int p = mPlayer.active_local_player;
int src = mPacketBuffer.PacketGetInt4(i);
int dst = mPacketBuffer.PacketGetInt4(i);
int seq = mPacketBuffer.PacketGetInt1(i);
int max_seq = mPacketBuffer.PacketGetInt1(i);
int sb = mPacketBuffer.PacketGetInt4(i);
int sz = mPacketBuffer.PacketGetInt4(i);
mLog.addf(LOG_NET_stdf_packets, p, "rx stdf piece [%d of %d] [%d to %d] st:%4d sz:%4d\n", seq+1, max_seq, src, dst, sb, sz);
mPlayer.loc[p].client_last_stdf_rx_frame_num = mLoop.frame_num; // client keeps track of last stdf rx'd and quits if too long
memcpy(client_state_buffer + sb, mPacketBuffer.rx_buf[i].data+22, sz); // put the piece of data in the buffer
client_state_buffer_pieces[seq] = dst; // mark it with destination mLoop.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)
{
// uncompress client_state_buffer to dif
uLongf destLen = sizeof(client_state_dif);
uncompress((Bytef*)client_state_dif, (uLongf*)&destLen, (Bytef*)client_state_buffer, sizeof(client_state_buffer));
if (destLen == STATE_SIZE)
{
mLog.addf(LOG_NET_stdf, p, "rx dif complete [%d to %d] dsync[%3.1fms] - uncompressed\n", src, dst, mPlayer.loc[p].dsync*1000);
client_state_dif_src = src; // mark dif data with new src and dst
client_state_dif_dst = dst;
}
else
{
mLog.addf(LOG_NET_stdf, p, "rx dif complete [%d to %d] dsync[%3.1f] - bad uncompress\n", src, dst, mPlayer.loc[p].dsync*1000);
client_state_dif_src = -1; // mark dif data as bad
client_state_dif_dst = -1;
}
}
}
How a client applies the new state
Every frame the client checks its locally stored dif.
First it checks if the dif is valid and newer than the last applied dif:
void mwNetgame::client_apply_dif(void)
{
int p = mPlayer.active_local_player;
mLog.addf(LOG_NET_dif_applied, p, "----- Apply dif [%d to %d] ", client_state_dif_src, client_state_dif_dst);
// check if dif is valid
if ((client_state_dif_src == -1) || (client_state_dif_dst == -1))
{
mLog.app(LOG_NET_dif_not_applied, "[not applied] [dif not valid]\n");
return;
}
// check if dif_dest has already been applied (check if dif_dest is less than or equal to newest_state_frame_num)
if (client_state_dif_dst <= mStateHistory[p].newest_state_frame_num)
{
mLog.app(LOG_NET_dif_not_applied, "[not applied] [not newer than last dif applied]\n");
return;
}
Next the dif destination is compared to the current frame number.
int ff = mPlayer.loc[p].rewind = mLoop.frame_num - client_state_dif_dst;
char tmsg[64];
if (ff == 0) sprintf(tmsg, "exact frame match [%d]", mLoop.frame_num);
if (ff > 0) sprintf(tmsg, "rewound [%d] frames", ff);
if (ff < 0)
{
if (mLoop.frame_num == 0) sprintf(tmsg, "initial state");
else sprintf(tmsg, "jumped ahead %d frames", -ff);
}
Next, we check to see if we have a base state that matches the dif source. (Also see State History)
char base[STATE_SIZE] = {0};
int base_frame_num = 0;
// finds and sets base matching 'client_state_dif_src' -- if not found, leaves base as is (zero)
mStateHistory[p].get_base_state(base, base_frame_num, client_state_dif_src);
if (base_frame_num == 0)
{
mPlayer.loc[p].client_base_resets++;
if (client_state_dif_src != 0)
{
int fn = mStateHistory[p].newest_state_frame_num;
mLog.appf(LOG_NET_dif_not_applied, "[not applied] [base not found] - resending stak [%d]\n", fn);
client_send_stak(fn);
return;
}
At this point we have everything needed to apply the dif.
But first we need to save some things so they can be restored after the dif is applied.
// ------------------------------------------------
// save things before applying dif
// ------------------------------------------------
// make a copy of level array l[][]
int old_l[100][100];
memcpy(old_l, mLevel.l, sizeof(mLevel.l));
// make a copy of players' pos
for (int pp=0; pp < NUM_PLAYERS; pp++)
if (mPlayer.syn[pp].active)
{
mPlayer.loc[pp].old_x = mPlayer.syn[pp].x;
mPlayer.loc[pp].old_y = mPlayer.syn[pp].y;
}
Finally we can apply the dif to the base state.
This just subtracts the dif from the base state.
// apply dif to base
apply_state_dif(base, client_state_dif, STATE_SIZE);
void mwNetgame::apply_state_dif(char *a, char *c, int size)
{
for (int i=0; i < size; i++) a[i] -= c[i];
}
Next, our modified base state is copied to the game variables and the current frame number is updated.
// copy to game vars and set new frame number
state_to_game_vars(base);
mLoop.frame_num = client_state_dif_dst;
void mwNetgame::state_to_game_vars(char * b)
{
int sz = 0, offset = 0;
sz = sizeof(mPlayer.syn); memcpy(mPlayer.syn, b+offset, sz); offset += sz;
sz = sizeof(mEnemy.Ei); memcpy(mEnemy.Ei, b+offset, sz); offset += sz;
sz = sizeof(mEnemy.Ef); memcpy(mEnemy.Ef, b+offset, sz); offset += sz;
sz = sizeof(mItem.item); memcpy(mItem.item, b+offset, sz); offset += sz;
sz = sizeof(mItem.itemf); memcpy(mItem.itemf, b+offset, sz); offset += sz;
sz = sizeof(mLift.cur); memcpy(mLift.cur, b+offset, sz); offset += sz;
sz = sizeof(mLevel.l); memcpy(mLevel.l, b+offset, sz); offset += sz;
sz = sizeof(mShot.p); memcpy(mShot.p, b+offset, sz); offset += sz;
sz = sizeof(mShot.e); memcpy(mShot.e, b+offset, sz); offset += sz;
sz = sizeof(mTriggerEvent.event); memcpy(mTriggerEvent.event, b+offset, sz); offset += sz;
}
Then this new state is saved in history to be used as a base for future difs.
// save to history
mStateHistory[p].add_state(mLoop.frame_num);
// keep track of frame number when last client dif was applied
mPlayer.loc[p].client_last_dif_applied = mLoop.frame_num;
// add log entry
mLog.appf(LOG_NET_dif_not_applied, "[applied] [%s]\n", tmsg);
Saved things from earlier are restored.
// ------------------------------------------------
// restore things after applying dif
// ------------------------------------------------
// fix control methods
mPlayer.syn[0].control_method = 2; // on client, server is always control method 2
if (mPlayer.syn[p].control_method == 2) mPlayer.syn[p].control_method = 4;
if (mPlayer.syn[p].control_method == 8) mLoop.state[0] = 1; // server quit
// compare old_l to l and redraw changed tiles
al_set_target_bitmap(mBitmap.level_background);
for (int x=0; x < 100; x++)
for (int y=0; y < 100; y++)
if (mLevel.l[x][y] != old_l[x][y])
{
al_draw_filled_rectangle(x*20, y*20, x*20+20, y*20+20, mColor.pc[0]);
al_draw_bitmap(mBitmap.btile[mLevel.l[x][y] & 1023], x*20, y*20, 0);
}
Next, if the state we just applied was from the past, we will fast forward to the current frame.
If the client has game moves saved for these frames, they will be applied as they are played back.
// ------------------------------------------------
// if we rewound time, play it back
// ------------------------------------------------
if (ff>0) mLoop.loop_frame(ff);
Next, we send an acknowledgment packet to let the server know we have applied the state and have a copy of it that can be used as a base for future difs the server sends.
Also we send various monitoring information about the client at the same time.
// send acknowledgment
client_send_stak(client_state_dif_dst);
void mwNetgame::client_send_stak(int ack_frame)
{
int p = mPlayer.active_local_player;
char data[1024] = {0}; int pos;
mPacketBuffer.PacketName(data, pos, "stak");
mPacketBuffer.PacketPutInt1(data, pos, p);
mPacketBuffer.PacketPutInt4(data, pos, ack_frame);
mPacketBuffer.PacketPutInt4(data, pos, mLoop.frame_num);
mPacketBuffer.PacketPutDouble(data, pos, mPlayer.loc[p].client_chase_fps);
mPacketBuffer.PacketPutDouble(data, pos, mPlayer.loc[p].dsync_avg);
mPacketBuffer.PacketPutInt1(data, pos, mPlayer.loc[p].rewind);
mPacketBuffer.PacketPutDouble(data, pos, mPlayer.loc[p].client_loc_plr_cor);
mPacketBuffer.PacketPutDouble(data, pos, mPlayer.loc[p].client_rmt_plr_cor);
mPacketBuffer.PacketPutDouble(data, pos, mPlayer.loc[p].cpu);
ClientSend(data, pos);
mLog.addf(LOG_NET_stak, p, "tx stak p:%d ack:[%d] cur:[%d]\n", p, ack_frame, mLoop.frame_num);
}
Now the fun part!!
Before we applied the dif, the players' positions were saved.
Now we compare them and calculate how much they were corrected.
This is a very good measure of how playable the netgame is.
If the corrections are large the players will appear to be warping around as they get corrected by new states from the server.
There are two types of corrections calculated. The local client's correction and the correction of any remote players.
These raw values are added to a Tally object, and once per second, the maximum and average are calculated.
You can view these variables on the debug overlay.
// ------------------------------------------------
// calc players' corrections
// ------------------------------------------------
mPlayer.loc[p].client_rmt_plr_cor = 0; // reset max remote
for (int pp=0; pp < NUM_PLAYERS; pp++)
if (mPlayer.syn[pp].active)
{
float cor = sqrt(pow((mPlayer.loc[pp].old_x - mPlayer.syn[pp].x), 2) + pow((mPlayer.loc[pp].old_y - mPlayer.syn[pp].y), 2)); // hypotenuse distance
if (p == pp) mPlayer.loc[p].client_loc_plr_cor = cor; // save local cor
else if (cor > mPlayer.loc[p].client_rmt_plr_cor) mPlayer.loc[p].client_rmt_plr_cor = cor; // save max remote cor
}
// add data to tally
mTally_client_loc_plr_cor_last_sec[p].add_data(mPlayer.loc[p].client_loc_plr_cor);
mTally_client_rmt_plr_cor_last_sec[p].add_data(mPlayer.loc[p].client_rmt_plr_cor);
}
// ----------------------------------------------------------
// do things based on the 1 Hz sec_timer event
// ----------------------------------------------------------
if (mNetgame.ima_client)
{
mNetgame.client_send_ping();
int p = mPlayer.active_local_player;
mPlayer.loc[p].client_loc_plr_cor_avg = mTally_client_loc_plr_cor_last_sec[p].get_avg(0);
mPlayer.loc[p].client_rmt_plr_cor_avg = mTally_client_rmt_plr_cor_last_sec[p].get_avg(0);
mPlayer.loc[p].client_loc_plr_cor_max = mTally_client_loc_plr_cor_last_sec[p].get_max(1);
mPlayer.loc[p].client_rmt_plr_cor_max = mTally_client_rmt_plr_cor_last_sec[p].get_max(1);
}
if (mNetgame.ima_server)
{
// tally late cdats and game move dsync
for (int p=1; p < NUM_PLAYERS; p++)
if (mPlayer.syn[p].control_method == 2)
{
mPlayer.syn[p].late_cdats_last_sec = mTally_late_cdats_last_sec[p].get_tally(1);
mPlayer.loc[p].game_move_dsync_avg_last_sec = mTally_game_move_dsync_avg_last_sec[p].get_avg(1);
mPlayer.loc[p].client_loc_plr_cor_avg = mTally_client_loc_plr_cor_last_sec[p].get_avg(0);
mPlayer.loc[p].client_rmt_plr_cor_avg = mTally_client_rmt_plr_cor_last_sec[p].get_avg(0);
mPlayer.loc[p].client_loc_plr_cor_max = mTally_client_loc_plr_cor_last_sec[p].get_max(1);
mPlayer.loc[p].client_rmt_plr_cor_max = mTally_client_rmt_plr_cor_last_sec[p].get_max(1);
}
}
The entire function 'client_apply_dif'
void mwNetgame::client_apply_dif(void)
{
int p = mPlayer.active_local_player;
mLog.addf(LOG_NET_dif_applied, p, "----- Apply dif [%d to %d] ", client_state_dif_src, client_state_dif_dst);
// check if dif is valid
if ((client_state_dif_src == -1) || (client_state_dif_dst == -1))
{
mLog.app(LOG_NET_dif_not_applied, "[not applied] [dif not valid]\n");
return;
}
// check if dif_dest has already been applied (check if dif_dest is less than or equal to newest_state_frame_num)
if (client_state_dif_dst <= mStateHistory[p].newest_state_frame_num)
{
mLog.app(LOG_NET_dif_not_applied, "[not applied] [not newer than last dif applied]\n");
return;
}
// if we got this far, we know that dif is valid and dif destination is newer than last applied dif
// compare dif destination to current frame number
int ff = mPlayer.loc[p].rewind = mLoop.frame_num - client_state_dif_dst;
char tmsg[64];
if (ff == 0) sprintf(tmsg, "exact frame match [%d]", mLoop.frame_num);
if (ff > 0) sprintf(tmsg, "rewound [%d] frames", ff);
if (ff < 0)
{
if (mLoop.frame_num == 0) sprintf(tmsg, "initial state");
else sprintf(tmsg, "jumped ahead %d frames", -ff);
}
// now check if we have a base state that matches dif source
char base[STATE_SIZE] = {0};
int base_frame_num = 0;
// finds and sets base matching 'client_state_dif_src' -- if not found, leaves base as is (zero)
mStateHistory[p].get_base_state(base, base_frame_num, client_state_dif_src);
if (base_frame_num == 0)
{
mPlayer.loc[p].client_base_resets++;
if (client_state_dif_src != 0)
{
int fn = mStateHistory[p].newest_state_frame_num;
mLog.appf(LOG_NET_dif_not_applied, "[not applied] [base not found] - resending stak [%d]\n", fn);
client_send_stak(fn);
return;
}
}
// ------------------------------------------------
// save things before applying dif
// ------------------------------------------------
// make a copy of level array l[][]
int old_l[100][100];
memcpy(old_l, mLevel.l, sizeof(mLevel.l));
// make a copy of players' pos
for (int pp=0; pp < NUM_PLAYERS; pp++)
if (mPlayer.syn[pp].active)
{
mPlayer.loc[pp].old_x = mPlayer.syn[pp].x;
mPlayer.loc[pp].old_y = mPlayer.syn[pp].y;
}
// apply dif to base
apply_state_dif(base, client_state_dif, STATE_SIZE);
// copy to game vars and set new frame number
state_to_game_vars(base);
mLoop.frame_num = client_state_dif_dst;
// keep track of frame number when last client dif was applied
mPlayer.loc[p].client_last_dif_applied = mLoop.frame_num;
// save to history
mStateHistory[p].add_state(mLoop.frame_num);
// add log entry
mLog.appf(LOG_NET_dif_not_applied, "[applied] [%s]\n", tmsg);
// ------------------------------------------------
// restore things after applying dif
// ------------------------------------------------
// fix control methods
mPlayer.syn[0].control_method = 2; // on client, server is always control method 2
if (mPlayer.syn[p].control_method == 2) mPlayer.syn[p].control_method = 4;
if (mPlayer.syn[p].control_method == 8) mLoop.state[0] = 1; // server quit
// compare old_l to l and redraw changed tiles
// double t0 = al_get_time();
al_set_target_bitmap(mBitmap.level_background);
for (int x=0; x < 100; x++)
for (int y=0; y < 100; y++)
if (mLevel.l[x][y] != old_l[x][y])
{
// printf("dif at x:%d y:%d\n", x, y);
al_draw_filled_rectangle(x*20, y*20, x*20+20, y*20+20, mColor.pc[0]);
al_draw_bitmap(mBitmap.btile[mLevel.l[x][y] & 1023], x*20, y*20, 0);
}
// mLog.add_log_TMR(al_get_time() - t0, "oldl", 0);
// ------------------------------------------------
// if we rewound time, play it back
// ------------------------------------------------
if (ff>0) mLoop.loop_frame(ff);
// send acknowledgment
client_send_stak(client_state_dif_dst);
// ------------------------------------------------
// calc players' corrections
// ------------------------------------------------
mPlayer.loc[p].client_rmt_plr_cor = 0; // reset max remote
for (int pp=0; pp < NUM_PLAYERS; pp++)
if (mPlayer.syn[pp].active)
{
float cor = sqrt(pow((mPlayer.loc[pp].old_x - mPlayer.syn[pp].x), 2) + pow((mPlayer.loc[pp].old_y - mPlayer.syn[pp].y), 2)); // hypotenuse distance
if (p == pp) mPlayer.loc[p].client_loc_plr_cor = cor; // save local cor
else if (cor > mPlayer.loc[p].client_rmt_plr_cor) mPlayer.loc[p].client_rmt_plr_cor = cor; // save max remote cor
}
// add data to tally
mTally_client_loc_plr_cor_last_sec[p].add_data(mPlayer.loc[p].client_loc_plr_cor);
mTally_client_rmt_plr_cor_last_sec[p].add_data(mPlayer.loc[p].client_rmt_plr_cor);
}