Purple Martians
Technical Code Descriptions
Netgame - Join
Overview
Program states for clients
PM_PROGRAM_STATE_CLIENT_NEW_GAME
Client initializes network
Client initiates join
Server receives 'cjon' packet and replies with 'sjon' packet
Client receives 'sjon' packet
PM_PROGRAM_STATE_CLIENT_LEVEL_SETUP
PM_PROGRAM_STATE_CLIENT_WAIT_FOR_INITIAL_STATE
PM_PROGRAM_STATE_CLIENT_EXIT
PM_PROGRAM_STATE_CLIENT_PREEXIT1
Overview
The client initializes the network and sends a join request.
The server replies with a join invitation.
The client waits for initial state from the server then joins the game.
Program states for clients
The program states used by clients are:
PM_PROGRAM_STATE_CLIENT_NEW_GAME
PM_PROGRAM_STATE_CLIENT_WAIT_FOR_JOIN
PM_PROGRAM_STATE_CLIENT_LEVEL_SETUP
PM_PROGRAM_STATE_CLIENT_WAIT_FOR_INITIAL_STATE
PM_PROGRAM_STATE_CLIENT_PREEXIT1
PM_PROGRAM_STATE_CLIENT_PREEXIT2
PM_PROGRAM_STATE_CLIENT_EXIT
PM_PROGRAM_STATE_CLIENT_NEW_GAME
if (state[1] == PM_PROGRAM_STATE_CLIENT_NEW_GAME)
{
mLog.add(LOG_OTH_program_state, 0, "[PM_PROGRAM_STATE_CLIENT_NEW_GAME]\n");
mLog.log_versions();
mLog.add_fw (LOG_NET, 0, 76, 10, "+", "-", "");
mLog.add_fwf(LOG_NET, 0, 76, 10, "|", " ", "Client mode started on localhost:[%s]", mLoop.local_hostname);
mEventQueue.reset_fps_timer();
for (int p=0; p < NUM_PLAYERS; p++) mPlayer.init_player(p, 1); // full reset
mPlayer.syn[0].active = 1;
if (!mNetgame.ClientInitNetwork())
{
state[0] = PM_PROGRAM_STATE_CLIENT_EXIT;
return;
}
if (!mNetgame.ClientJoin())
{
state[0] = PM_PROGRAM_STATE_CLIENT_EXIT;
return;
}
mRollingAverage[1].initialize(8); // ping rolling average
mRollingAverage[2].initialize(8); // dsync rolling average
state[0] = PM_PROGRAM_STATE_CLIENT_LEVEL_SETUP;
}
Client initializes network
When a client game is started, the first thing that happens is the network initialization.
int mwNetgame::ClientInitNetwork(void)
{
char msg[512];
if (NetworkInit())
{
sprintf(msg, "Error: failed to initialize network");
mLog.add_fw(LOG_error, 0, 76, 10, "|", " ", msg);
mInput.m_err(msg);
return -1;
}
Channel = net_openchannel(NetworkDriver, NULL); // dynamic port
if (Channel == NULL)
{
sprintf(msg, "Error: Client failed to create NetChannel");
mLog.add_fw(LOG_error, 0, 76, 10, "|", " ", msg);
mInput.m_err(msg);
return 0;
}
char target[300];
sprintf(target, "%s:%d", server_address, server_port);
if (net_assigntarget(Channel, target))
{
sprintf(msg, "Error: Client failed to set NetChannel target:[%s]", target);
mLog.add_fw(LOG_error, 0, 76, 10, "|", " ", msg);
mInput.m_err(msg);
return 0;
}
mLog.add_fwf(LOG_NET, 0, 76, 10, "|", " ", "Client network initialized -- target:[%s]", target);
mLog.add_fwf(LOG_NET, 0, 76, 10, "|", " ", "Local address:[%s]", net_getlocaladdress(Channel));
return 1;
}
Client initiates join
After the network is setup, the client initiates the join request.
It sends a 'cjon' packet and waits for a 'sjon' reply from the server.
int mwNetgame::ClientJoin(void)
{
char msg[512];
// Check for reply from server
int tries = 1; // number of times to try
float try_delay = 1; // delay between tries
int got_reply = 0;
while (!got_reply)
{
//mLog.add_fwf(LOG_NET, 0, 76, 10, "|", " ", "ClientCheckResponse %d", tries);
int ccr = ClientCheckResponse();
if (ccr == 1)
{
got_reply = 1;
// mLog.add_fw(LOG_NET, 0, 76, 10, "|", " ", "Got 'join accepted' reply from server");
}
if (ccr == 0)
{
al_rest(try_delay);
if (++tries > 2)
{
sprintf(msg, "Did not get reply from server");
mLog.add_fw(LOG_error, 0, 76, 10, "|", " ", msg);
mInput.m_err(msg);
mLog.add_fw(LOG_NET, 0, 76, 10, "+", "-", "");
return 0;
}
}
if (ccr == -1)
{
sprintf(msg, "Got 'server full' reply from server");
mLog.add_fw(LOG_error, 0, 76, 10, "|", " ", msg);
mLog.add_fw(LOG_NET, 0, 76, 10, "+", "-", "");
mInput.m_err(msg);
return 0;
}
if (ccr == -2)
{
mLog.add_fw(LOG_NET, 0, 76, 10, "|", " ", "Cancelled");
mLog.add_fw(LOG_NET, 0, 76, 10, "+", "-", "");
return 0;
}
}
ima_client = 1;
return 1;
}
int mwNetgame::ClientCheckResponse(void) // check for a response from the server
{
client_send_cjon_packet();
mLog.add_fw(LOG_NET, 0, 76, 10, "|", " ", "Sent initial 'cjon' packet to server");
mLog.add_fw(LOG_NET, 0, 76, 10, "|", " ", "Waiting for reply");
al_set_target_backbuffer(mDisplay.display);
al_clear_to_color(al_map_rgb(0,0,0));
mScreen.rtextout_centref(mFont.pr8, NULL, mDisplay.SCREEN_W/2, 2*mDisplay.SCREEN_H/4, 15, -2, 1, "Waiting for Server:[%s]", server_address);
mScreen.rtextout_centre( mFont.pr8, NULL, mDisplay.SCREEN_W/2, 3*mDisplay.SCREEN_H/4, 15, -2, 1, " ESC to cancel ");
al_flip_display();
char prog[41];
sprintf(prog, " ");
char data[1024];
char address[32];
int done = 0;
while (done < 40)
{
mEventQueue.proc(1);
if (mInput.key[ALLEGRO_KEY_ESCAPE][1]) return -2; // cancelled
if (net_receive(Channel, data, 1024, address))
{
if (mPacketBuffer.PacketRead(data, "sjon"))
{
return client_proc_sjon_packet(data); // 1=join, -1=full
}
}
prog[done] = '.';
mScreen.rtextout_centref(mFont.pr8, NULL, mDisplay.SCREEN_W/2, 5*mDisplay.SCREEN_H/8, 15, -2, 1, "%s", prog);
al_flip_display();
al_rest(.1);
done++;
}
return 0; // no response
}
Server receives 'cjon' packet and replies with 'sjon' packet
When the server receives a 'cjon' packet, it checks to see if it has an available slot for the client.
If the server is full, it replies with an 'sjon' packet indicating the server is full.
If the server is not full, it proceeds with the join process.
First it checks if it can use the requested color.
Then the server sets up a client player structure and replies with an 'sjon' packet.
void mwNetgame::server_proc_cjon_packet(char *data, char * address)
{
int pos = 4;
int color = mPacketBuffer.PacketGetByte(data, pos);
char hostname[16];
mPacketBuffer.PacketReadString(data, pos, hostname);
mLog.add_fwf(LOG_NET, 0, 76, 10, "+", "-", "");
mLog.add_fwf(LOG_NET, 0, 76, 10, "|", " ", "Server received join request from %s requesting color:%d", hostname, color);
int p = mPlayer.find_inactive_player();
if (p == 99) // no inactive player found
{
mLog.add_fwf(LOG_NET, 0, 76, 10, "|", " ", "Reply sent: 'SERVER FULL'");
mLog.add_fwf(LOG_NET, 0, 76, 10, "+", "-", "");
server_send_sjon_packet(address, 0, 0, 99, 0);
if (mLog.log_types[LOG_NET_session].action) session_add_entry(address, hostname, 99, 0, 1);
}
else // inactive player found, proceed with join
{
// set up channel, use the player number as the index to the channel
mwChannels[p].active = 1;
strcpy(mwChannels[p].address, address);
// try to use requested color, unless already used by another player
while (mPlayer.is_player_color_used(color)) if (++color > 15) color = 1;
mStateHistory[p].initialize();
mPlayer.init_player(p, 1); // full player reset
mItem.set_player_start_pos(p);
mPlayer.syn[p].active = 1;
mPlayer.syn[p].control_method = PM_PLAYER_CONTROL_METHOD_NETGAME_REMOTE;
mPlayer.loc[p].server_last_stak_rx_frame_num = mLoop.frame_num + 200;
sprintf(mPlayer.loc[p].hostname, "%s", hostname);
mGameMoves.add_game_move(mLoop.frame_num, PM_GAMEMOVE_TYPE_PLAYER_ACTIVE, p, color); // add game move to make client active
server_send_sjon_packet(address, mLevel.play_level, mLoop.frame_num, p, color);
if (mLog.log_types[LOG_NET_session].action) session_add_entry(address, hostname, p, 1, 0);
mLog.add_fwf(LOG_NET, 0, 76, 10, "|", " ", "Server replied with join invitation:");
mLog.add_fwf(LOG_NET_join_details, 0, 76, 10, "|", " ", "Level:[%d]", mLevel.play_level);
mLog.add_fwf(LOG_NET_join_details, 0, 76, 10, "|", " ", "Player Number:[%d]", p);
mLog.add_fwf(LOG_NET_join_details, 0, 76, 10, "|", " ", "Player Color:[%d]", color);
mLog.add_fwf(LOG_NET_join_details, 0, 76, 10, "|", " ", "Server Frame:[%d]", mLoop.frame_num);
mLog.add_fwf(LOG_NET_join_details, 0, 76, 10, "|", " ", "Server Level Sequence Num:[%d]", mPlayer.syn[0].server_lev_seq_num);
mLog.add_fwf(LOG_NET, 0, 76, 10, "+", "-", "");
}
}
Client receives 'sjon' packet
When the client receives the 'sjon' packet, it sets up the local player structure and moves to the next phase of joining.
int mwNetgame::client_proc_sjon_packet(char * data)
{
int pos = 4;
int pl = mPacketBuffer.PacketGetInt4(data, pos); // play level
int sfnum = mPacketBuffer.PacketGetInt4(data, pos); // server join frame number
int p = mPacketBuffer.PacketGetByte(data, pos); // client player number
int color = mPacketBuffer.PacketGetByte(data, pos); // client player color
int slsn = mPacketBuffer.PacketGetByte(data, pos); // server level sequence number
if (p == 99) return -1; // server full, join denied
else // join allowed
{
mPlayer.active_local_player = p;
mPlayer.syn[p].active = 1;
mPlayer.syn[p].control_method = PM_PLAYER_CONTROL_METHOD_CLIENT_LOCAL;
mPlayer.syn[p].color = color;
mPlayer.syn[0].server_lev_seq_num = slsn;
strncpy(mPlayer.loc[0].hostname, server_address, 16);
strncpy(mPlayer.loc[p].hostname, mLoop.local_hostname, 16);
mLevel.play_level = pl;
mLog.add_fwf(LOG_NET, 0, 76, 10, "|", " ", "Client received join invitation from server");
mLog.add_fwf(LOG_NET_join_details, 0, 76, 10, "|", " ", "Level:[%d]", mLevel.play_level);
mLog.add_fwf(LOG_NET_join_details, 0, 76, 10, "|", " ", "Player Number:[%d]", p);
mLog.add_fwf(LOG_NET_join_details, 0, 76, 10, "|", " ", "Player Color:[%d]", color);
mLog.add_fwf(LOG_NET_join_details, 0, 76, 10, "|", " ", "Server Frame Num:[%d]", sfnum);
mLog.add_fwf(LOG_NET_join_details, 0, 76, 10, "|", " ", "Server Level Sequence Num:[%d]", slsn);
mLog.add_fwf(LOG_NET, 0, 76, 10, "+", "-", "");
return 1;
}
}
PM_PROGRAM_STATE_CLIENT_LEVEL_SETUP
Here the client loads the level, sets up for the game, then moves to the next step.
if (state[1] == PM_PROGRAM_STATE_CLIENT_LEVEL_SETUP)
{
mLog.add(LOG_OTH_program_state, 0, "[PM_PROGRAM_STATE_CLIENT_LEVEL_SETUP]\n");
if (load_and_setup_level(mLevel.play_level, 2)) state[0] = PM_PROGRAM_STATE_CLIENT_WAIT_FOR_INITIAL_STATE;
else state[0] = PM_PROGRAM_STATE_CLIENT_EXIT;
}
PM_PROGRAM_STATE_CLIENT_WAIT_FOR_INITIAL_STATE
The client blocks in this state, processing packets and waiting for the initial state from the server.
Once the client receives the initial state, it advances to PM_PROGRAM_STATE_MAIN_GAME_LOOP.
if (state[1] == PM_PROGRAM_STATE_CLIENT_WAIT_FOR_INITIAL_STATE)
{
mLog.add(LOG_OTH_program_state, 0, "[PM_PROGRAM_STATE_CLIENT_WAIT_FOR_INITIAL_STATE]\n");
mScreen.rtextout_centre(mFont.bltn, NULL, mDisplay.SCREEN_W/2, mDisplay.SCREEN_H/2, 10, -2, 1, "Waiting for game state from server");
al_flip_display();
if (mInput.key[ALLEGRO_KEY_ESCAPE][3]) state[0] = PM_PROGRAM_STATE_CLIENT_EXIT;
mPacketBuffer.rx_and_proc();
mNetgame.client_apply_dif();
if (frame_num > 0) state[0] = PM_PROGRAM_STATE_MAIN_GAME_LOOP;
}
PM_PROGRAM_STATE_CLIENT_EXIT
To shutdown and cleanup gracefully after a client netgame, we go to PM_PROGRAM_STATE_CLIENT_EXIT.
if (state[1] == PM_PROGRAM_STATE_CLIENT_EXIT)
{
mLog.add(LOG_OTH_program_state, 0, "[PM_PROGRAM_STATE_CLIENT_EXIT]\n");
mNetgame.ClientExitNetwork();
quit_action = 1; // to prevent quitting clients from automatically going to overworld
state[0] = PM_PROGRAM_STATE_MENU;
}
void mwNetgame::ClientExitNetwork(void)
{
mLog.add_header(LOG_NET, 0, 0, "Shutting down the client network");
if (Channel) net_closechannel(Channel);
Channel = NULL;
net_shutdown();
ima_client = 0;
// reset player data
for (int p=0; p < NUM_PLAYERS; p++) mPlayer.init_player(p, 1);
mPlayer.syn[0].active = 1; // local_control
mPlayer.active_local_player = 0;
mConfig.load_config(); // to restore colors and other settings
}
PM_PROGRAM_STATE_CLIENT_PREEXIT1
If the client chooses to quit we go through some pre-exit states first to optionally save the game.
if (state[1] == PM_PROGRAM_STATE_CLIENT_PREEXIT1)
{
if ((mGameMoves.autosave_game_on_level_quit) && (mLevel.play_level != 1)) // not overworld
{
// when client quits with escape, send a file request to the server to get the gm file
mNetgame.client_send_crfl();
state[0] = PM_PROGRAM_STATE_CLIENT_PREEXIT2;
}
else state[0] = PM_PROGRAM_STATE_CLIENT_EXIT;
}
if (state[1] == PM_PROGRAM_STATE_CLIENT_PREEXIT2)
{
mScreen.rtextout_centre(mFont.bltn, NULL, mDisplay.SCREEN_W/2, mDisplay.SCREEN_H/2, 10, -2, 1, "Waiting for file from server");
al_flip_display();
mPacketBuffer.rx_and_proc();
// when file is received, or srrf packet is received indicating file will not be sent
// state will be advanced from CLIENT_PREEXIT2 to CLIENT_EXIT
mEventQueue.proc(1);
if (mInput.key[ALLEGRO_KEY_ESCAPE][2]) state[0] = PM_PROGRAM_STATE_CLIENT_EXIT; // give an escape option
}