Purple Martians
Technical Code Descriptions
Netgame - State History
Overview
Data Structure
Adding a new state to history
Server rewinds state
Server makes a new state
Server sends dif states to clients
Server gets acknowledgment from client
Client applies dif state
Overview
State history is an array of objects used to save and manage a history of states.
Both the server and clients make use of it.
Clients save states to history whenever they apply a new state from the server.
Then later they use those old states as a base to apply new dif states.
The server keeps a history of states that it uses to rewind and replay when it receives late input from clients.
The server also keeps a history of states it has sent to each client that are used to make new dif states for that client.
Data Structure
The class 'mwNetgame' owns an array of 8 objects of class 'mwStateHistory', one corresponding to each player.
index 0 is used by the server for rewind states
index 1-7 is used by the server for client base states
The clients only use one index for base states (the index that corresponds to their local player number)
class mwNetgame
{
mwStateHistory mStateHistory[8];
...
Each object has 8 history states.
This is a trade off between the amount of memory used and the number of saved states.
8 * 8 * 112384 = 7,192,576 bytes
#define NUM_HISTORY_STATES 8
#define STATE_SIZE 112384
class mwStateHistory
{
char history_state[NUM_HISTORY_STATES][STATE_SIZE];
...
Adding a new state to history
Both the client and the server use it.
It takes the current game variables and passed frame number and adds it to state history.
This is the only method to add or change state history.
Whenever it is called, it also checks and sets oldest and newest states.
// if a state already exists with exact frame number, overwrite it
// if not replace the oldest frame number (include empty -1's so they get used first)
// this is the only way that anything gets added or changed
void mwStateHistory::add_state(int frame_num)
{
int indx = -1;
// if a state already exists with exact frame number, use that index and overwrite it
for (int i=0; i < NUM_HISTORY_STATES; i++) if (history_state_frame_num[i] == frame_num) indx = i;
if (indx == -1)
{
// find lowest frame number, include -1 so they will be used first if any exist
int mn = std::numeric_limits::max();
for (int i=0; i < NUM_HISTORY_STATES; i++)
if (history_state_frame_num[i] < mn)
{
mn = history_state_frame_num[i];
indx = i;
}
}
if (indx > -1)
{
mNetgame.game_vars_to_state(history_state[indx]);
history_state_frame_num[indx] = frame_num;
}
_set_newest_and_oldest();
// check if we just invalidated last ack state by adding a new state
if (last_ack_state_frame_num > -1) set_ack_state(last_ack_state_frame_num);
}
// called whenever adding state
void mwStateHistory::_set_newest_and_oldest(void)
{
int mn = std::numeric_limits::max();
int mx = std::numeric_limits::min();
int mn_indx = -1;
int mx_indx = -1;
for (int i=0; i < NUM_HISTORY_STATES; i++)
{
int fn = history_state_frame_num[i];
if (fn > -1) // ignore all unset states
{
if (fn < mn) // new minimum
{
mn = fn;
mn_indx = i;
}
if (fn > mx) // new maximum
{
mx = fn;
mx_indx = i;
}
}
}
if (mn_indx > -1)
{
oldest_state_frame_num = mn;
oldest_state_index = mn_indx;
oldest_state = history_state[mn_indx];
}
if (mx_indx > -1)
{
newest_state_frame_num = mx;
newest_state_index = mx_indx;
newest_state = history_state[mx_indx];
}
}
Server rewinds state
This is how the server rewinds state and replays back to current frame.
void mwNetgame::server_rewind(void)
{
...
mStateHistory[0].apply_rewind_state(server_dirty_frame);
...
}
void mwStateHistory::apply_rewind_state(int frame_num)
{
if (frame_num < 1) return;
// how many frames to rewind and replay
int ff = mPlayer.loc[0].rewind = mLoop.frame_num - frame_num;
// if same frame as current frame, do nothing
if (ff == 0)
{
mLog.addf(LOG_NET_stdf, 0, "stdf rewind [none]\n");
return;
}
// find index of matching frame
int indx = -1;
for (int i=0; i < NUM_HISTORY_STATES; i++) if (frame_num == history_state_frame_num[i]) indx = i;
if (indx == -1)
{
mLog.addf(LOG_NET_stdf, 0, "stdf rewind [%d] not found - ", frame_num);
indx = oldest_state_index;
if (indx == -1) mLog.app(LOG_NET_stdf, "oldest frame not valid\n");
else mLog.appf(LOG_NET_stdf, "using oldest frame [%d]\n", history_state_frame_num[indx]);
}
if (indx > -1)
{
mLog.addf(LOG_NET_stdf, 0, "stdf rewind to:%d [%d]\n", frame_num, -ff);
mNetgame.state_to_game_vars(history_state[indx]);
mLoop.frame_num = history_state_frame_num[indx];
// fast forward and save rewind states
for (int i=0; i < ff; i++)
{
mLoop.loop_frame(1);
add_state(mLoop.frame_num);
}
}
}
Server makes a new state
This is how the server makes a new state.
void mwNetgame::server_create_new_state(void)
{
// this state is created at the end of the frame, after moves have been applied
// because of that it is actually the starting point of the next frame
// so it is sent and saved with frame_num + 1
// it is done this way, rather than wait for the start of the next frame
// because after the end of the frame, there is a pause before the next frame starts
// and I want to send new state as soon as possible
int frame_num = mLoop.frame_num + 1;
server_dirty_frame = frame_num;
// this is the server rewind state, not the client base
mStateHistory[0].add_state(frame_num);
mLog.addf(LOG_NET_stdf, 0, "stdf save state:%d\n", frame_num);
server_send_dif(frame_num); // send to all clients
}
Server sends dif states to clients
This is how the server sends a dif states to each active client.
void mwNetgame::server_send_dif(int frame_num) // send dif to all clients
{
for (int p=1; p < NUM_PLAYERS; p++)
if ((mPlayer.syn[p].control_method == 2) || (mPlayer.syn[p].control_method == 8))
{
// save current state in history as base for next clients send
mStateHistory[p].add_state(frame_num);
char base[STATE_SIZE] = {0};
int base_frame_num = 0;
// get client's most recent base state (the last one acknowledged to the server)
// if not found, leaves base as is (zero)
mStateHistory[p].get_last_ack_state(base, base_frame_num);
if (base_frame_num == 0) mPlayer.loc[p].client_base_resets++;
// make a new dif from base and current
char dif[STATE_SIZE];
get_state_dif(base, mStateHistory[p].newest_state, dif, STATE_SIZE);
// break into packet and send to client
server_send_compressed_dif(p, base_frame_num, frame_num, dif);
}
}
// called when the server is making a dif to send to a client and needs a base to build it on
void mwStateHistory::get_last_ack_state(char* base, int& base_frame_num)
{
int indx = last_ack_state_index;
if (indx > -1)
{
memcpy(base, history_state[indx], STATE_SIZE);
base_frame_num = history_state_frame_num[indx];
}
}
Server gets acknowledgment from client
When the server gets an acknowledgment from a client.
void mwNetgame::server_proc_stak_packet(int i)
{
...
int ack_frame_num = mPacketBuffer.PacketGetInt4(i); // client has acknowledged getting and applying this base
mStateHistory[p].set_ack_state(ack_frame_num);
...
// called when the server receives stak packet acknowledging frame_num
// if we have a state that matches that frame_num, set last_ack variables
// if we do not, then reset them
void mwStateHistory::set_ack_state(int frame_num)
{
// see if we have a state with this frame_num
int indx = -1;
for (int i=0; i < NUM_HISTORY_STATES; i++) if (frame_num == history_state_frame_num[i]) indx = i;
if (indx > -1)
{
last_ack_state_frame_num = frame_num;
last_ack_state_index = indx;
last_ack_state = history_state[indx];
}
else
{
last_ack_state_frame_num = -1;
last_ack_state_index = -1;
last_ack_state = NULL;
}
}
Client applies dif state
When the client applies a dif state from the server.
void mwNetgame::client_apply_dif(void)
{
...
// 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_packet(fn);
return;
}
}
// 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;
// save to history
mStateHistory[p].add_state(mLoop.frame_num);
// if we rewound time, play it back
if (ff > 0) mLoop.loop_frame(ff);
// send acknowledgment
client_send_stak_packet(client_state_dif_dst);
}
// called when client needs a base state to apply a dif to
// searches for a match for passed frame_num
void mwStateHistory::get_base_state(char* base, int& base_frame_num, int frame_num)
{
if (frame_num == 0) return; // base 0 leave as is
int indx = -1;
for (int i=0; i -1)
{
memcpy(base, history_state[indx], STATE_SIZE);
base_frame_num = history_state_frame_num[indx];
}
}