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
   }