Purple Martians
Technical Code Descriptions

Game Moves Array
Overview Variables Array elements Types of game moves How game_move entries are added How controls are set from game_move array How game_move entries are added in netgame
Overview The game moves array is the heart of the entire game control system. It contains every input from every player, indexed by the frame number. The array does not store an entry for every frame, only when a player's controls have changed. When a player's control inputs change, a new entry is made into the array at that frame number. In another function, the game move array is used to set players' controls when the frame numbers match. This method is not needed for single player mode, but it is essential for netgame and demo game replaying. So, in the interests of keeping things consistant, it is also the method used for single player. Variables
#define GAME_MOVES_SIZE 1000000
class mwGameMoves
{
   ...
   int arr[GAME_MOVES_SIZE][4];
   int entry_pos = 0;
   int current_pos = 0;
The game moves array can hold 1 million game moves. 1 million might seem excessive but with 8 players at 40fps, you could theoretically get 320 game moves per second. (In reality a human player can't press the controls fast enough to make 40 moves per second!) But at the worst case, that's 19200 game_moves per minute or 1,152,000 game_moves per hour.
entry_pos;
The current entry position of the array. When a new move is added, it is added at this position, and then this position is incremented.
current_pos;
The current read position of the array. Array elements Each game move entry has 4 elements:
mGameMoves.arr[x][0]; // frame_num
mGameMoves.arr[x][1]; // game move type
mGameMoves.arr[x][2]; // player number
mGameMoves.arr[x][3]; // comp_move
A player's controls are encoded into a bitfield called 'comp_move' with these values:
#define PM_COMPMOVE_LEFT   0b00000001
#define PM_COMPMOVE_RIGHT  0b00000010
#define PM_COMPMOVE_UP     0b00000100
#define PM_COMPMOVE_DOWN   0b00001000
#define PM_COMPMOVE_JUMP   0b00010000
#define PM_COMPMOVE_FIRE   0b00100000
#define PM_COMPMOVE_MENU   0b01000000
Types of game moves
#define PM_GAMEMOVE_TYPE_LEVEL_START      0
#define PM_GAMEMOVE_TYPE_PLAYER_ACTIVE    1
#define PM_GAMEMOVE_TYPE_PLAYER_INACTIVE  2
#define PM_GAMEMOVE_TYPE_PLAYER_HIDDEN    3
#define PM_GAMEMOVE_TYPE_PLAYER_MOVE      5
#define PM_GAMEMOVE_TYPE_LEVEL_DONE_ACK   8
#define PM_GAMEMOVE_TYPE_SHOT_CONFIG      9
PM_GAMEMOVE_TYPE_LEVEL_START
mGameMoves.arr[x][0]; // frame_num
mGameMoves.arr[x][1]; // game_move type = PM_GAMEMOVE_TYPE_LEVEL_START
mGameMoves.arr[x][2]; // player number (0)
mGameMoves.arr[x][3]; // level
PM_GAMEMOVE_TYPE_PLAYER_ACTIVE
mGameMoves.arr[x][0]; // frame_num
mGameMoves.arr[x][1]; // game_move type = PM_GAMEMOVE_TYPE_PLAYER_ACTIVE
mGameMoves.arr[x][2]; // player number
mGameMoves.arr[x][3]; // color
PM_GAMEMOVE_TYPE_PLAYER_INACTIVE
mGameMoves.arr[x][0]; // frame_num
mGameMoves.arr[x][1]; // game_move type = PM_GAMEMOVE_TYPE_PLAYER_INACTIVE
mGameMoves.arr[x][2]; // player number
mGameMoves.arr[x][3]; // reason
PM_GAMEMOVE_TYPE_PLAYER_HIDDEN
mGameMoves.arr[x][0]; // frame_num
mGameMoves.arr[x][1]; // game_move type = PM_GAMEMOVE_TYPE_PLAYER_HIDDEN
mGameMoves.arr[x][2]; // player number
mGameMoves.arr[x][3]; // unused
PM_GAMEMOVE_TYPE_MOVE
mGameMoves.arr[x][0]; // frame_num
mGameMoves.arr[x][1]; // game_move type = PM_GAMEMOVE_TYPE_MOVE
mGameMoves.arr[x][2]; // player number
mGameMoves.arr[x][3]; // comp move
PM_GAMEMOVE_TYPE_LEVEL_DONE_ACK
mGameMoves.arr[x][0]; // frame_num
mGameMoves.arr[x][1]; // game_move type = PM_GAMEMOVE_TYPE_LEVEL_DONE_ACK
mGameMoves.arr[x][2]; // player number
mGameMoves.arr[x][3]; // usused
PM_GAMEMOVE_TYPE_SHOT_CONFIG
mGameMoves.arr[x][0]; // frame_num
mGameMoves.arr[x][1]; // game_move type = PM_GAMEMOVE_TYPE_SHOT_CONFIG
mGameMoves.arr[x][2]; // pvp and pvs shot enable bitfield
mGameMoves.arr[x][3]; // shot damage
How game_move entries are added
void mwPlayer::proc_player_input(void)
{
   for (int p=0; p < NUM_PLAYERS; p++)
      if (syn[p].active) // cycle all active players
      {
         set_comp_move_from_player_key_check(p);
         if (loc[p].comp_move != comp_move_from_players_current_controls(p))   // player's controls have changed
         {
            mGameMoves.add_game_move(mLoop.frame_num, PM_GAMEMOVE_TYPE_PLAYER_MOVE, p, loc[p].comp_move); // add to game moves array
....

void mwPlayer::set_comp_move_from_player_key_check(int p)
{
   int cm = 0;
   if (mInput.key[loc[0].left_key][0])       cm |= PM_COMPMOVE_LEFT;
   if (mInput.key[loc[0].right_key][0])      cm |= PM_COMPMOVE_RIGHT;
   if (mInput.key[loc[0].up_key][0])         cm |= PM_COMPMOVE_UP;
   if (mInput.key[loc[0].down_key][0])       cm |= PM_COMPMOVE_DOWN;
   if (mInput.key[loc[0].jump_key][0])       cm |= PM_COMPMOVE_JUMP;
   if (mInput.key[loc[0].fire_key][0])       cm |= PM_COMPMOVE_FIRE;
   if (mInput.key[loc[0].menu_key][0])       cm |= PM_COMPMOVE_MENU;
   if (mInput.key[ALLEGRO_KEY_ESCAPE][0])    cm |= PM_COMPMOVE_MENU;
   loc[p].comp_move = cm;
}

void mwGameMoves::add_game_move(int frame, int type, int data1, int data2)
{
   arr[entry_pos][0] = frame;
   arr[entry_pos][1] = type;
   arr[entry_pos][2] = data1;
   arr[entry_pos][3] = data2;
   entry_pos++;
}
How controls are set from game_move array Player's controls are set by searching the game_move array for frame numbers that match the current frame.
// this function processes all entries in the game_moves array that match current frame number
void mwGameMoves::proc(void)
{
   // search entire range
   int start_index = entry_pos-1;
   int end_index = 0;

   if (!(mLoop.state[1] == PM_PROGRAM_STATE_DEMO_RECORD))
   {
      // reduce search range
      start_index = current_pos + 100;
      if (start_index > entry_pos-1) start_index = entry_pos-1;

      end_index = current_pos - 100;
      if (end_index < 0) end_index = 0;
   }

   for (int x=start_index; x>=end_index; x--)  // search backwards from start_index to end_index
   {
      if (arr[x][0] == mLoop.frame_num) // found frame number that matches current frame
      {
         if (!(mLoop.state[1] == PM_PROGRAM_STATE_DEMO_RECORD))
            if (x > current_pos) current_pos = x; // index of last used gm - keep this at the most recent one, never go back

         switch (arr[x][1])
         {
            case PM_GAMEMOVE_TYPE_PLAYER_ACTIVE:    proc_game_move_player_active(        arr[x][2], arr[x][3] ); break;
            case PM_GAMEMOVE_TYPE_PLAYER_INACTIVE:  proc_game_move_player_inactive(      arr[x][2], arr[x][3] ); break;
            case PM_GAMEMOVE_TYPE_PLAYER_HIDDEN:    proc_game_move_player_hidden(        arr[x][2]            ); break;
            case PM_GAMEMOVE_TYPE_SHOT_CONFIG:      proc_game_move_shot_config(          arr[x][2], arr[x][3] ); break;
            case PM_GAMEMOVE_TYPE_PLAYER_MOVE:      mPlayer.set_controls_from_comp_move( arr[x][2], arr[x][3] ); break;
         }
      }
   }

void mwPlayer::set_controls_from_comp_move(int p, int comp_move)
{
   clear_controls(p);
   if (comp_move & PM_COMPMOVE_LEFT)  syn[p].left  = 1;
   if (comp_move & PM_COMPMOVE_RIGHT) syn[p].right = 1;
   if (comp_move & PM_COMPMOVE_UP)    syn[p].up    = 1;
   if (comp_move & PM_COMPMOVE_DOWN)  syn[p].down  = 1;
   if (comp_move & PM_COMPMOVE_JUMP)  syn[p].jump  = 1;
   if (comp_move & PM_COMPMOVE_FIRE)  syn[p].fire  = 1;
   if (comp_move & PM_COMPMOVE_MENU)  syn[p].menu  = 1;
}
How game_move entries are added in netgame In netgame, things are done differently. In client mode, when a control change occurs, a 'cdat' packet is sent to the server, and the move is also applied directly to controls:
if (syn[p].control_method == PM_PLAYER_CONTROL_METHOD_CLIENT_LOCAL)
{
   mNetgame.client_send_cdat_packet(p);
   set_controls_from_comp_move(p, loc[p].comp_move);
}

void mwNetgame::client_send_cdat_packet(int p)
{
   char data[PACKET_BUFFER_SIZE] = {0}; int pos;
   mPacketBuffer.PacketName(data, pos, "cdat");
   mPacketBuffer.PacketPutByte(data, pos, p);
   mPacketBuffer.PacketPutInt4(data, pos, mLoop.frame_num);
   mPacketBuffer.PacketPutByte(data, pos, mPlayer.loc[p].comp_move);
   ClientSend(data, pos);
}

void mwPlayer::set_controls_from_comp_move(int p, int comp_move)
{
   clear_controls(p);
   if (comp_move & PM_COMPMOVE_LEFT)  syn[p].left  = 1;
   if (comp_move & PM_COMPMOVE_RIGHT) syn[p].right = 1;
   if (comp_move & PM_COMPMOVE_UP)    syn[p].up    = 1;
   if (comp_move & PM_COMPMOVE_DOWN)  syn[p].down  = 1;
   if (comp_move & PM_COMPMOVE_JUMP)  syn[p].jump  = 1;
   if (comp_move & PM_COMPMOVE_FIRE)  syn[p].fire  = 1;
   if (comp_move & PM_COMPMOVE_MENU)  syn[p].menu  = 1;
}
When the server receives the packet, the client's game move is put in the server's master game_move array.
void mwNetgame::server_proc_cdat_packet(int i)
{
   int p              = mPacketBuffer.PacketGetByte(i);
   int cdat_frame_num = mPacketBuffer.PacketGetInt4(i);
   int cm             = mPacketBuffer.PacketGetByte(i);
   mGameMoves.add_game_move(cdat_frame_num, PM_GAMEMOVE_TYPE_PLAYER_MOVE, p, cm);
}