/*
* Descent 3 
* Copyright (C) 2024 Parallax Software
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program.  If not, see <http://www.gnu.org/licenses/>.

--- HISTORICAL COMMENTS FOLLOW ---

 * $Logfile: /DescentIII/Main/dmfc/dmfcclient.cpp $
 * $Revision: 1.1.1.1 $
 * $Date: 2003/08/26 03:57:20 $
 * $Author: kevinb $
 *
 * Client Event handlers
 *
 * $Log: dmfcclient.cpp,v $
 * Revision 1.1.1.1  2003/08/26 03:57:20  kevinb
 * initial 1.5 import
 *
 *
 * 92    9/05/01 6:04p Matt
 * Added code to save the user's preferred HUD name level setting even if
 * the server bashes it down.
 *
 * 91    7/15/99 3:33p Jeff
 * play a sound when a player disconnects/leaves the game
 *
 * 90    7/13/99 12:11p Jeff
 * added some specific text taunt token decoding
 *
 * 89    7/13/99 10:05a Jeff
 * text taunt token decoding
 *
 * 88    7/09/99 7:02p Jeff
 * put in countdown timer for when a level is about to end
 *
 * 87    7/09/99 6:17p Jeff
 * added $remoteadminlogout and $wait commands
 *
 * 86    7/09/99 2:53p Jeff
 * handle gametime better (pause it when needed) if the server is 'waiting
 * for players'
 *
 * 85    7/08/99 9:56p Jeff
 * added event handler for weapon fired event
 *
 * 84    7/08/99 2:39a Jeff
 * rough implementation of remote administration checked in.  Still needs
 * some polishing, but should work basically.
 *
 * 83    7/07/99 6:10p Jeff
 * don't count the time that a player is in observer mode for death/kill
 * messages
 *
 * 82    6/22/99 5:41p Jeff
 * removed int3 (causing probs in Linux)
 *
 * 81    6/11/99 5:36p Jeff
 * removed ai_info #ifdefs (better way of doing it)
 *
 * 80    6/10/99 12:41p Jeff
 * no ai_info in non-Outrage builds
 *
 * 79    5/24/99 11:05p Jeff
 * fixed bug for dedicated server team getting reset on endlevel
 *
 * 78    5/11/99 9:42p Jeff
 * fixed bug changing teams while dead
 *
 * 77    5/10/99 2:43a Jeff
 * handle new scheme of player's joining in a team game, where the team is
 * set before player enters game in the main code, but the team is
 * determined via event call to dmfc
 *
 * 76    5/08/99 4:45a Jeff
 * fixed printscreen position
 *
 * 75    5/08/99 4:30a Jeff
 * fixed sequencing bug where clients never got a level end event for the
 * multiplayer games
 *
 * 74    5/07/99 5:16p Jeff
 * fixed player disconnect at level end
 *
 * 73    5/07/99 2:21p Jeff
 * fixed (?) not disconnecting clients on level end
 *
 * 72    5/04/99 8:46p Jeff
 * display icon when someone plays an audio taunt
 *
 * 71    4/03/99 4:06p Jeff
 * added loss/ping gauge
 *
 * 70    3/30/99 7:42p Jeff
 * fixed a misspelling on a function name
 *
 * 69    3/22/99 6:20p Jeff
 * added 2 more audio taunts.  a mulitplayer event when someone plays an
 * audio taunt.  option to disable audio taunts.
 *
 * 68    3/22/99 1:55p Jeff
 * make sure initialization happens (possible crashing)
 *
 * 67    3/17/99 12:23p Jeff
 * converted DMFC to be COM interface
 *
 * 66    2/11/99 12:50a Jeff
 * changed names of exported variables
 *
 * 65    2/09/99 3:32p Jeff
 * table file parser takes quotes strings for force keywords
 *
 * 64    2/07/99 1:19a Jeff
 * added new multiplayer game events EVT_GAMEOBJKILLED and
 * EVT_GAMEOBJDESTROYED
 *
 * 63    2/05/99 8:24p Jeff
 * added table file parser macros
 *
 * 62    2/04/99 7:17p Jeff
 * put in sounds for hud messages
 *
 * 61    2/02/99 8:43a Chris
 * I made buildings with AI work correctly (ie really big robots should be
 * buildings)
 * anim to and from states are now shorts instead of bytes
 *
 * 60    2/01/99 10:52p Jeff
 * added sounds for kills (teammate and regular)
 *
 * 59    1/31/99 7:26p Matt
 * Renamed a bunch of functions to have HUD capitalized
 *
 * 58    1/19/99 3:55a Jeff
 * all strings localized out
 *
 * 56    1/15/99 3:54a Jeff
 * exported a couple more functions.  Added event handlers for Weapon
 * collide event
 *
 * 55    1/12/99 2:55p Jeff
 * added/finished the waiting for player's to join dialog
 *
 * 54    1/10/99 7:41p Jeff
 * added initial version of waitforplayers dialog
 *
 * 53    1/07/99 5:01p Jeff
 * added Int3's and updated all net games to use stats manager...correctly
 * too
 *
 * 52    1/06/99 7:02p Jeff
 * added a multiplayer event for game controls
 *
 * 51    12/23/98 6:38p Kevin
 * All UDP data (except gamespy) now uses one (registered) port number
 *
 * 50    12/08/98 4:47p Jeff
 * umm, various changes, fixed pilot pics so that they work for everyone
 * now
 *
 * 49    12/08/98 3:29p Jeff
 * updated the team control dialog so the server can determine if they
 * want to make the clients wait
 *
 * 48    12/08/98 12:17p Jeff
 * various changes that include an improved Team Control dialog (doesn't
 * switch teams until exit..) and spew/respawn players that change teams
 *
 * 47    12/03/98 7:05p Jeff
 * updated new stats
 *
 * 46    12/01/98 6:56p Jeff
 * put in quick and dirty implementation of pilot pics for testing
 *
 * 45    11/19/98 5:56p Jeff
 * added slider exported and improved Hoard
 *
 * 44    11/17/98 6:29p Jeff
 * mod can specify whether or not to display the team setup dialog on team
 * game start.  Added a menu item to display team setup dialog in mid-game
 *
 * 43    11/16/98 5:35p Jeff
 * removed log functions, added support for changing team names, fixed ctf
 * to work better with different team names
 *
 * 42    11/11/98 7:19p Jeff
 * changes made so that a dedicated server's team is always -1 (team game
 * or not)
 *
 * 41    11/11/98 12:47p Jeff
 * fixed bug where it would give team change messages in a non-team game
 *
 * 40    11/01/98 1:59a Jeff
 * made a $help inputcommand for help in a dedicated server environment
 *
 * 39    10/30/98 12:47p Jeff
 * cut down a couple bytes on memory usage
 *
 * 38    10/29/98 7:01p Jeff
 * creation of team placement dialog.  Moved TranslateEvent into DMFC
 *
 * 37    10/20/98 12:16p Jeff
 * added death message filter, hud callsign filter
 *
 * 36    10/18/98 7:59p Jeff
 * functions added to dmfc for client->server objnum matching.  Banning
 * now uses tracker id when available.
 *
 * 35    10/15/98 6:18p Jeff
 * created the is player banned event, removed prejoin event
 *
 * 34    10/15/98 1:34p Jeff
 * added scrollable onscreen menu.  Remove ban in dmfc.  prejoin event
 *
 * 33    10/13/98 2:15a Jeff
 * created new event for when a player leaves a multiplayer game.  Fixed
 * some 'english' bugs in the network games.
 *
 * 32    10/11/98 2:57a Jeff
 * added new multiplayer event EVT_GAME_INTERVAL, which is to be called on
 * interval, and EVT_HUD_INTERVAL is only to be called when the hud is to
 * be rendered
 *
 * 31    10/08/98 3:37p Jeff
 * general improvements (Netgame info things, save to registry).  Changes
 * so it would send packets on NETSEQ_OBJECTS
 *
 * 30    10/05/98 2:49p Jeff
 *
 * 29    10/01/98 7:02p Jeff
 * implemented colored HUD callsigns
 *
 * 28    10/01/98 11:30a Jeff
 * made the observer mode events into just a client event
 *
 * 27    9/30/98 4:21p Jeff
 * team changing is handled correctly
 *
 * 26    9/30/98 4:07p Jeff
 *
 * 25    9/30/98 3:50p Jeff
 * general improvements (many)
 *
 * 24    9/28/98 5:05p Jeff
 * made the statisitical death messages an option in the menu
 *
 * 23    9/25/98 7:57p Jeff
 * removed CheckPInfo call
 *
 * 22    9/25/98 7:25p Jeff
 *
 * 21    9/25/98 4:50p Jeff
 *
 * 20    9/24/98 6:54p Jeff
 * added DisconnectMe() and made use of it when you get kicked/banned
 *
 * 19    9/24/98 5:52p Jeff
 * starting adding statistical death messages, checked in for testing
 *
 * 18    9/23/98 4:56p Jeff
 * set teams always for 1 team game
 *
 * 17    9/23/98 4:17p Jeff
 * basic changes/improvements, started changing death messages
 *
 * 16    9/21/98 7:11p Jeff
 * made InputCommand interface API and moved existing input commands to
 * the interface.  Changed mprintf/ASSERT so they are valid in DMFC
 *
 * $NoKeywords: $
 */

#include "gamedll_header.h"
#include "DMFC.h"
#include "dmfcinternal.h"

#define SND_MYTEAM_TEAMMATE_KILLED "PlayerDeath"
#define SND_NONTEAM_KILL "PlayerDeath"

/*
$$TABLE_SOUND "PlayerDeath"
*/

// ######################### Client allowed event handlers############################

// DMFCBase::OnClientPlayerKilled (Only called if server tells client to execute)
//
//  Event handler for when a player gets killed, either by another player or some other way.
//  killer_obj = object pointer to the object that killed the player
//  victim_pnum = player number of the player that got killed
void DMFCBase::OnClientPlayerKilled(object *killer_obj, int victim_pnum) {
  uint32_t hash = 0xFFFFFFFF;
  int kpnum;
  player_record *kpr, *vpr;

  if (!killer_obj) {
    kpnum = -1;
  } else {
    if ((killer_obj->type == OBJ_PLAYER) || (killer_obj->type == OBJ_GHOST) || (killer_obj->type == OBJ_OBSERVER)) {
      kpnum = killer_obj->id;

      // now we need to extract out the weapon of the kill
      if (Data->iParam != -1)
        hash = (uint32_t)Data->iParam;
    } else if (killer_obj->type == OBJ_ROBOT || (killer_obj->type == OBJ_BUILDING && killer_obj->ai_info)) {
      // countermeasure kill
      kpnum = GetCounterMeasureOwner(killer_obj);
    } else
      kpnum = -1;
  }

  if (victim_pnum != -1) {
    if (kpnum == victim_pnum) {
      // suicide
      vpr = PRec_GetPRecordByPnum(victim_pnum);
      if (vpr) {
        vpr->dstats.suicides[DSTAT_LEVEL]++;
        vpr->dstats.suicides[DSTAT_OVERALL]++;
      }
      UpdatePInfo(kpnum, kpnum, 1);
    } else {
      // regular death
      vpr = PRec_GetPRecordByPnum(victim_pnum);

      if (vpr) {
        vpr->dstats.deaths[DSTAT_LEVEL]++;
        vpr->dstats.deaths[DSTAT_OVERALL]++;
      }

      if (kpnum != -1) {
        kpr = PRec_GetPRecordByPnum(kpnum);
        if (kpr) {
          kpr->dstats.kills[DSTAT_LEVEL]++;
          kpr->dstats.kills[DSTAT_OVERALL]++;
        }
        UpdatePInfo(victim_pnum, kpnum, 1);
      }
    }
  }

  static int teammate_kill_id = -2;
  static int nonteam_kill_id = -2;

  if (killer_obj && killer_obj->type == OBJ_PLAYER && victim_pnum != -1) {

    if (Players[killer_obj->id].team == Players[victim_pnum].team && (GetNumTeams() >= 2) &&
        (killer_obj->id != victim_pnum)) {
      // a teammate killed another teammate!
      if (teammate_kill_id == -2) {
        teammate_kill_id = DLLFindSoundName(IGNORE_TABLE(SND_MYTEAM_TEAMMATE_KILLED));
      }
      if (teammate_kill_id != -1) {
        DLLPlay2dSound(teammate_kill_id, MAX_GAME_VOLUME / 2);
      }
    } else {
      // normal kill sound
      if (nonteam_kill_id == -2) {
        nonteam_kill_id = DLLFindSoundName(IGNORE_TABLE(SND_NONTEAM_KILL));
      }
      if (nonteam_kill_id != -1) {
        DLLPlay2dSound(nonteam_kill_id, MAX_GAME_VOLUME / 2);
      }
    }
  }

  // if DMFC is supposed to handle Deathmessages, than do one
  if (m_iProtectedFlags & DMFC_PRF_AUTODEATHMSG)
    DoRandomDeathMessage(GetItObjNum(), GetMeObjNum(), hash);
}

// DMFCBase::OnClientPlayerExploded (Only called if server tells client to execute)
//
//  Event handler for when a player explodes.  Gets called after a EVT_GAMEPLAYERKILLED event.
//  player_num = player number of the player exploding
void DMFCBase::OnClientPlayerExploded(int player_num) {}

// DMFCBase::OnClientCollide (Only called if server tells client to execute)
//
//   Event handler for when two objects collide.  At least one of the two objects is a player or a robot.
//   Be aware that this event will be called twice per collision, the second time it is called the me and
//   it objects will be flipped.
//	 ALSO NOTE: In order for the OnClientCollide() that passes the point and normal to have valid values
//	 the server must pass true to the parameter to send arguments in CallClientEvent().
//   me_obj = object pointer to the me object
//   it_obj = object pointer to the it object
void DMFCBase::OnClientCollide(object *me_obj, object *it_obj) {}
void DMFCBase::OnClientCollide(object *me_obj, object *it_obj, vector *point, vector *normal) {}

// DMFCBase::OnClientPlayerChangeSegment (Only called if server tells client to execute)
//
//   Event handler for when a player changes rooms or a "large" terrain cell
//   (8x8 Terrain block).
//   player_num = player number of the player who just changed the room/segment
//   newseg = New room/cell location
//	 oldseg = Old room/cell location
void DMFCBase::OnClientPlayerChangeSegment(int player_num, int newseg, int oldseg) {}

// DMFCBase::OnClientObjectChangeSegment (Only called if server tells client to execute)
//
//   Event handler for when a player changes rooms or a "large" terrain cell
//   (8x8 Terrain block).
//   obj = Object pointer of the object who just changed the room/segment
//   newseg = New room/cell location
//	 oldseg = Old room/cell location
void DMFCBase::OnClientObjectChangeSegment(object *obj, int newseg, int oldseg) {}

// DMFCBase::OnClientPlayerEntersGame (Only called if server tells client to execute)
//
//   Event handler for when a player enters the game.  This will only get called once per
//   player, it usually gets called right after they connect to the server to start playing.
//   player_num = player number of the player entering the game
void DMFCBase::OnClientPlayerEntersGame(int player_num) {
  if ((GetLocalRole() == LR_SERVER) || (player_num != GetPlayerNum())) {
    int slot;

    if (IsMasterTrackerGame())
      slot = PRec_FindPlayer(Players[player_num].callsign, NULL, Players[player_num].tracker_id);
    else
      slot = PRec_FindPlayer(Players[player_num].callsign, &NetPlayers[player_num].addr, NULL);

    if (slot == -1) {
      // we have a new player
      slot = PRec_GetFreeSlot();
      if (!PRec_AssignPlayerToSlot(player_num, slot, Players, NetPlayers))
        mprintf(0, "Unable to assign New Player (%s) to Player Record slot #%d\n", Players[player_num].callsign, slot);
      else
        mprintf(0, "(%s) has been assigned to Player Record slot #%d\n", Players[player_num].callsign, slot);
      CallOnPlayerConnect(player_num);
    } else {
      // we have a reconnecting player
      if (!PRec_ReconnectPlayerToSlot(player_num, slot, Players, NetPlayers))
        mprintf(0, "Unable to reassign reconnecting player (%s) to Player Record slot #%d\n",
                 Players[player_num].callsign, slot);
      else
        mprintf(0, "Reconnected player (%s) to Player Record slot #%d\n", Players[player_num].callsign, slot);
      CallOnPlayerReconnect(player_num);
    }
  }

  // If the correct options are set, go into the D1/D2 style wait screen
  if (GetLocalRole() == LR_SERVER && (player_num == GetPlayerNum())) {

    if (GetNumTeams() > 1 && (!(m_iProtectedFlags & DMFC_PRF_NOTEAMSELECTONSTART))) {
      // Display the team placement dialog
      static char enable;
      enable = 1;
      StartUIWindow(UIID_TEAMPLACEMENT, &enable);
    }

    if (GetNumTeams() < 2 && (!(m_iProtectedFlags & DMFC_PRF_NOWAITFORPLAYERS))) {
      StartUIWindow(UIID_WAITFORPLAYERS, NULL);
    }
  }

  // Load up the pilot picture bitmap
  //--------------------------------
  if (player_num == GetPlayerNum()) {
    // since we are entering the game, go through all the players and setup their bitmaps
    for (int w = 0; w < DLLMAX_PLAYERS; w++) {
      if (CheckPlayerNum(w) || (w == 0 && GetLocalRole() == LR_SERVER)) {
        if (PilotPicBmpHandles[w] > BAD_BITMAP_HANDLE) {
          DLLbm_FreeBitmap(PilotPicBmpHandles[w]);
          PilotPicBmpHandles[w] = BAD_BITMAP_HANDLE;
        }

        mprintf(0, "PPIC ID: %d is %d\n", w, NetPlayers[w].pilot_pic_id);
        if (NetPlayers[w].pilot_pic_id != 65535) {
          PilotPicBmpHandles[w] = DLLPPic_GetBitmapHandle(NetPlayers[w].pilot_pic_id);
          if (PilotPicBmpHandles[w] < BAD_BITMAP_HANDLE)
            PilotPicBmpHandles[w] = BAD_BITMAP_HANDLE;
        } else {
          PilotPicBmpHandles[w] = BAD_BITMAP_HANDLE;
        }
      } else {
        PilotPicBmpHandles[w] = BAD_BITMAP_HANDLE;
      }
    }
  } else {
    if (PilotPicBmpHandles[player_num] > BAD_BITMAP_HANDLE) {
      DLLbm_FreeBitmap(PilotPicBmpHandles[player_num]);
      PilotPicBmpHandles[player_num] = BAD_BITMAP_HANDLE;
    }

    mprintf(0, "PPIC ID: %d is %d\n", player_num, NetPlayers[player_num].pilot_pic_id);
    if (NetPlayers[player_num].pilot_pic_id != 65535) {
      PilotPicBmpHandles[player_num] = DLLPPic_GetBitmapHandle(NetPlayers[player_num].pilot_pic_id);
      if (PilotPicBmpHandles[player_num] < BAD_BITMAP_HANDLE)
        PilotPicBmpHandles[player_num] = BAD_BITMAP_HANDLE;
    } else {
      PilotPicBmpHandles[player_num] = BAD_BITMAP_HANDLE;
    }
  }

  // Play the player hud message sound
  static int sound_id = -2;
  if (sound_id == -2) {
    sound_id = DLLFindSoundName("Hudmessage");
  }

  if (sound_id > -1 && GetPlayerNum() != player_num) {
    DLLPlay2dSound(sound_id, MAX_GAME_VOLUME / 2);
  }
}

// DMFCBase::OnClientPlayerDisconnect
//
//   Event handler for when a player disconnects from the server.
//   player_num = player number of the player that just disconnected
void DMFCBase::OnClientPlayerDisconnect(int player_num) {
  // Play the player hud message sound
  static int sound_id = -2;
  if (sound_id == -2) {
    sound_id = DLLFindSoundName("Hudmessage");
  }

  if (sound_id > -1 && GetPlayerNum() != player_num) {
    DLLPlay2dSound(sound_id, MAX_GAME_VOLUME / 2);
  }

  // save the team
  PRec_SetPlayerTeam(player_num, Players[player_num].team);

  if (!PRec_DisconnectPlayer(player_num))
    mprintf(0, "Unable to disconnect player (%s) from Player Records\n", Players[player_num].callsign);
  else
    mprintf(0, "Disconnected player (%s) from Player Records\n", Players[player_num].callsign);
}

// DMFCBase::OnMeDisconnectFromServer
//
//	Event handler that gets called if we disconnect from the server for some reason (not purposesly quit)
void DMFCBase::OnMeDisconnectFromServer(void) {
  // We're disconnecting so save the stats to file
  if (m_iProtectedFlags & DMFC_PRF_AUTOSAVEDISC)
    CallOnDisconnectSaveStatsToFile();
}

// DMFCBase::OnClientGameCreated (Only called if server tells client to execute)
//
//   Event handler when the server's game first gets started.  This will only be called once
//   while the server is running, and that is when the server first starts up.
void DMFCBase::OnClientGameCreated(void) {}

// DMFCBase::OnClientLevelChange (Only called if server tells client to execute)
//
//   Event handler when the server changes levels.  This will get called after a level ends and a server
//   is starting a new level.
void DMFCBase::OnClientLevelChange(void) {}

// DMFCBase::OnClientLevelStart (Only called if server tells client to execute)
//
//   Event handler for when a multiplayer level is starting up.  This will get called right before the level
//   starts.
void DMFCBase::OnClientLevelStart(void) {
  // Clear this array so we don't get weird anomalies
  ResetPlayersInGame();

  // Reset the timers
  InitTimers();

  // Reset level stats of Player Records
  PRec_LevelReset();

  // reset the pinfo list
  ResetPInfo();

  // Request Player Records
  SendRequestForPlayerRecords();

  DisplayTauntIndicator = false;
}

// DMFCBase::OnClientLevelEnd
//
//   Event handler for when a multiplayer level is ending.
void DMFCBase::OnClientLevelEnd(void) {
  MarkPlayersInGame();

  if (m_iProtectedFlags & DMFC_PRF_AUTOSAVELEVELEND)
    CallOnLevelEndSaveStatsToFile();

  KillAllTimers();
  ENABLE_FLAGS(m_iProtectedFlags, DMFC_PRF_PLRFIRSTTIME);

  // Call PLRInit so data can be initialized
  CallOnPLRInit();

  for (int i = 0; i < DLLMAX_PLAYERS; i++) {
    if ((NetPlayers[i].flags & NPF_CONNECTED) &&
        (NetPlayers[i].sequence >= NETSEQ_PLAYING || NetPlayers[i].sequence == NETSEQ_WAITING_FOR_LEVEL)) {

      // save the team
      PRec_SetPlayerTeam(i, Players[i].team);

      if (!PRec_DisconnectPlayer(i))
        mprintf(0, "Unable to disconnect player (%s) from Player Records\n", Players[i].callsign);
      else
        mprintf(0, "Disconnected player (%s) from Player Records\n", Players[i].callsign);
    }
  }
}

// DMFCBase::OnPlayerEntersObserver
//
//	Event handler when a player becomes an observer mode
//	If they are piggybacking another player than piggy is the object pointer, else it's NULL
void DMFCBase::OnPlayerEntersObserver(int pnum, object *piggy) {
  mprintf(0, "Player %d entering observermode %s\n", pnum, (piggy) ? "Piggyback" : "Roam");
  player_record *pr = PRec_GetPRecordByPnum(pnum);
  if (pr && pr->state == STATE_INGAME) {
    PInfo *pi = pr->pinfo;
    ASSERT(pi != NULL);
    pi->EnterObserverMode();
  } else {
    Int3();
  }
}

// DMFCBase::OnPlayerExitsObserver
//
//	Event handler when a player is leaving observer mode
void DMFCBase::OnPlayerExitsObserver(int pnum) {
  mprintf(0, "Player %d leaving observer mode\n", pnum);
  player_record *pr = PRec_GetPRecordByPnum(pnum);
  if (pr && pr->state == STATE_INGAME) {
    PInfo *pi = pr->pinfo;
    ASSERT(pi != NULL);
    pi->ExitObserverMode();
  } else {
    Int3();
  }
}

// DMFCBase::OnCanChangeTeam
//
//    Called to determine if a player can change teams, you can override this so the server won't
//    let a player change teams if it is not desired (for that player)
bool DMFCBase::OnCanChangeTeam(int pnum, int newteam) {
  if (Players[pnum].team == -1)
    return false; // no team switching for dedicated server

  // AnnounceTeamChangeDeny(pnum);
  return true;
}

// DMFCBase::OnSaveStatsToFile
//
//
//		The user is requesting the game stats to be saved to file, you must handle this completly on
//	the game's side, nothing is done in DMFC
void DMFCBase::OnSaveStatsToFile(void) { mprintf(0, "User requested stats to be saved!\n"); }

// DMFCBase::OnLevelEndSaveStatsToFile
//
//
//	The game should save the "End of Level" stats to file.  You must handle this completly on
//	the game's side, nothing is done in DMFC
void DMFCBase::OnLevelEndSaveStatsToFile(void) {}

// DMFCBase::OnDisconnectSaveStatsToFile
//
//
//	The game should save the stats because the player (only ourself as the client in the game),
//	has disconnected.
void DMFCBase::OnDisconnectSaveStatsToFile(void) {}

// DMFCBase::OnSpecialPacket
//
//   Event handler for when a special packet arrives and needs to be processed.
//   Both the server and client can get this event, although it is more common for the
//   client to recieve these.
void DMFCBase::OnSpecialPacket(void) {
  if (!SPRoot)
    return;

  // see if we have a handler for the ID, if so, call the handler, else do nothing
  uint8_t *data = Data->special_data;
  int id = data[0];

  tSPHandler *current;
  current = SPRoot;
  while (current) {
    if (current->ID == id) {
      // we got a winner
      switch (current->type) {
      case SPH_DMFCFUNC:
        if (current->DMFCfunc)
          (this->*current->DMFCfunc)(data + 1);
        break;
      case SPH_FUNC:
        if (current->func)
          (*current->func)(data + 1);
        break;
      }
      return;
    }
    current = current->next;
  }
}

// DMFCBase::OnHUDInterval
//
//	Event handler that gets called once a frame when it's time to render the HUD images
void DMFCBase::OnHUDInterval(void) {
  if (m_bMakeClientsWait)
    WaitingForServerLoop();
  if (m_iProtectedFlags & DMFC_PRF_DISPPLAYERINFO)
    DisplayPlayerInfo(MenuBackgroundBMP);
  if (m_iProtectedFlags & DMFC_PRF_DISPNETGAMEINFO)
    DisplayNetGameInfo(MenuBackgroundBMP);
  LossGuageFrame();
  DisplayTauntIndicatorFrame();

  if (m_iProtectedFlags & DMFC_PRF_INOPTIONS) {
    // Update all the state submenus so they are reporting the correct value
    if (TeamMenuItem)
      TeamMenuItem->SetState(GetPlayerTeam(GetPlayerNum()));
    if (AutoBalanceItem)
      AutoBalanceItem->SetState((m_iProtectedFlags & DMFC_PRF_AUTOTEAMSELECT) ? 1 : 0);
    if (AllowTeamChangeItem)
      AllowTeamChangeItem->SetState(AllowTeamChange() ? 1 : 0);
    if (ObserverItem)
      ObserverItem->SetState((Objects[Players[GetPlayerNum()].objnum].type == OBJ_OBSERVER) ? 1 : 0);
    if (StatisticMessagesItem)
      StatisticMessagesItem->SetState((m_iProtectedFlags & DMFC_PRF_DISPSTATHUDMSGS) ? 1 : 0);
    if (SaveStatsLevelEndItem)
      SaveStatsLevelEndItem->SetState((m_iProtectedFlags & DMFC_PRF_AUTOSAVELEVELEND) ? 1 : 0);
    if (SaveStatsDisconnectItem)
      SaveStatsDisconnectItem->SetState((m_iProtectedFlags & DMFC_PRF_AUTOSAVEDISC) ? 1 : 0);
    if (MenuBackgroundItem)
      MenuBackgroundItem->SetState((m_iProtectedFlags & DMFC_PRF_DISPMENUBACKGR) ? 1 : 0);
    if (NetGameInfoItem)
      NetGameInfoItem->SetState(IsDisplayingNetGameInfo() ? 1 : 0);
    if (HUDCallsignsItem)
      HUDCallsignsItem->SetState(m_iMyCurrentHUDCallsignLevel);
    if (ServerHUDCallsignsItem)
      ServerHUDCallsignsItem->SetState(m_iServerHUDCallsignLevel);
    if (DeathMessageFilterItem)
      DeathMessageFilterItem->SetState(m_iDeathMessageFilter);
    if (ShipLogosItem)
      ShipLogosItem->SetState(AreLogosEnabled() ? 1 : 0);
    if (HUDIndicatorItem)
      HUDIndicatorItem->SetState(LossGuageEnabled ? 1 : 0);

    // We have an options screen up, so draw it
    DrawMenu();
  }

  // DisplayPilotPicsFrame();

  // Display countdown if needed
  DisplayTimelimitCountdown();

  DLLgrtext_Flush();
}

// DMFCBase::OnInterval
//
//   Event handler that gets called once a frame
void DMFCBase::OnInterval(void) {
  if (!(m_iProtectedFlags & DMFC_PRF_PAUSETIME)) {
    RealGametime += *Frametime; // increment the timer
  }

  // If we are a dedicated server, we are waiting, and there is a wait time
  // check to see if it's time to disable
  if (GetLocalRole() == LR_SERVER && IAmDedicatedServer() && m_bMakeClientsWait) {
    if (DedicatedLevelWait > 0 && DedicatedLevelWait <= (*Gametime)) {
      // time to stop waiting!
      mprintf(0, "Telling clients that they can play!\n");
      DPrintf("Allowing Clients To Play\n");
      PauseRealGameTime(false);
      MakeClientsWait(false);
    }
  }

  // CheckPInfo();

  // Process remote admin frame
  Remote_ProcessFrame();

  // Process all currently running timers
  ProcessTimers();

  // Handle time limit games
  float tleft;
  if (GetLocalRole() == LR_SERVER) {
    if ((m_iProtectedFlags & DMFC_PRF_AUTOTIMELIMIT) && (GetTimeLeft(&tleft))) {
      if (tleft <= 0) {
        EndLevel();
        return;
      }
    }
  }
}

// DMFCBase::OnPLRInterval
//
//  Event handler that gets called once a frame when the Post Level Results screen is being display
void DMFCBase::OnPLRInterval(void) {
  DLLgrtext_SetFont(Game_fonts[SMALL_UI_FONT_INDEX]);
  DLLgrtext_SetColor(GR_RGB(255, 40, 40));
  DLLgrtext_CenteredPrintf(0, 13, Netgame->scriptname);
  DLLgrtext_CenteredPrintf(0, 461, DTXT_PRESSPRNTSCRN);
}

// DMFCBase::OnPLRInit
//
//  Event handler that gets called the first frame of the PLR screen for each level
void DMFCBase::OnPLRInit(void) { DISABLE_FLAGS(m_iProtectedFlags, DMFC_PRF_PLRFIRSTTIME); }

// DMFCBase::OnKeypress
//
//   Event handler for when a user presses a key while in the game
//   key = Key code of the key being pressed
void DMFCBase::OnKeypress(int key) {
  if (m_iProtectedFlags & DMFC_PRF_INOPTIONS) {
    Data->iRet = 1;
    // Handle keyboard input for an options menu
    if (key == K_F6 || key == K_ESC) { // escape out of options
      DISABLE_FLAGS(m_iProtectedFlags, DMFC_PRF_INOPTIONS);
    }
    if (key == K_ENTER) {
      Menu.Execute();
    }
    switch (key) {
    case K_DOWN: {
      Menu.Down();
    } break;
    case K_UP: {
      Menu.Up();
    } break;
    case K_RIGHT: {
      Menu.Forward();
    } break;
    case K_LEFT: {
      Menu.Back();
    } break;
    }
  } else if (key == K_F6) {
    ENABLE_FLAGS(m_iProtectedFlags, DMFC_PRF_INOPTIONS);
  }
}

// DMFCBase::OnInputString
//
//   Event handler for when the user types a message at the console (F8) that begins with a $
//   input_string = string that was typed
void DMFCBase::OnInputString(char *input_string) {
  if (!InputCommandHandle(input_string)) {
    // The input command wasn't handled!
    mprintf(0, "DMFC Warning: Input Command '%s' wasn't handled\n", input_string);
  }
}

// DMFCBase::OnPlayerChangeTeam
//
//  Called when a player changes team
void DMFCBase::OnPlayerChangeTeam(int player_num, int newteam, bool announce, bool spew_everything) {
  player_record *pr = GetPlayerRecordByPnum(player_num);
  if (pr) {
    if (GetNumTeams() > 1 && announce && newteam != -1)
      DLLAddHUDMessage(DTXT_CHANGETEAM, pr->callsign, GetTeamString(newteam));
    pr->team = newteam;

    // only respawn them if they are not dead
    if (!((Players[player_num].flags & PLAYER_FLAGS_DYING) || (Players[player_num].flags & PLAYER_FLAGS_DEAD))) {
      RespawnPlayer(player_num, false, spew_everything);
    }
  }
}

// DMFCBase::OnClientObjectShieldsChanged
//
//	Event handler for when an objects shield's change
void DMFCBase::OnClientObjectShieldsChanged(object *obj, float amount) {}

// DMFCBase::OnWeaponFired
//
//	Event handler for when an object fires a weapon
void DMFCBase::OnWeaponFired(object *weapon_obj, object *shooter) {}

// DMFCBase::OnPlayerReconnect
//
//
//	This player is reconnecting to the game
void DMFCBase::OnPlayerReconnect(int player_num) {
  if (GetLocalRole() == LR_SERVER) {
    // restore their team
    int team = PRec_GetPlayerTeam(player_num);
    if (team != Players[player_num].team) {
      // hey! Jason hasn't set the correct team for this player
      ASSERT(player_num == 0); // this is only going to happen for the server
    }

    SendTeamAssignment(player_num, team, false);
    Players[player_num].team = team;
    CallOnPlayerChangeTeam(player_num, team, false, false);
    mprintf(0, "Reassigning (%s) to %s team\n", Players[player_num].callsign, GetTeamString(team));
  }
}

// DMFCBase::OnPlayerConnect
//
//
//	This player is connecting to the game for the first time
void DMFCBase::OnPlayerConnect(int player_num) {
  if (GetLocalRole() == LR_SERVER) {

    // a dedicated server MUST be given a -1 team
    //------------------------------------------
    if (player_num == 0 && IAmDedicatedServer()) {
      Players[0].team = -1;
    }

    // get team assignment
    if (m_iProtectedFlags & DMFC_PRF_AUTOTEAMSELECT) {
      // JEFF: Commented out since team's are now determined right when a client connects
      //@@int team = GetTeamForNewPlayer(player_num,m_iNumTeams);
      int team = Players[player_num].team;
      SendTeamAssignment(player_num, team, false);
      //@@Players[player_num].team = team;
      CallOnPlayerChangeTeam(player_num, team, false, false);
      mprintf(0, "Assigning (%s) to %s team\n", Players[player_num].callsign, GetTeamString(team));
    }
  }
}

// DMFCBase::OnControlMessage
//
//
//	There is a control message sent from someone
void DMFCBase::OnControlMessage(uint8_t msg, int from_pnum) {
  switch (msg) {
  case CM_KICKED: {
    DLLAddHUDMessage(DTXT_KICKEDMSG);
    DisconnectMe();
  } break;
  case CM_BANNED: {
    DLLAddHUDMessage(DTXT_BANNEDMSG);
    DisconnectMe();
  } break;
  case CM_TEAMCHANGEDENIED: {
    // if we get this, the server is telling us that the request for a
    // team change has been denied
    AnnounceTeamChangeDeny(*Player_num);
  } break;
  case CM_SIGNALSTART: {
    if (from_pnum == 0) {
      // we can move!!
      WaitForServer(false);
    }
  } break;
  case CM_SIGNALWAIT: {
    if (from_pnum == 0) {
      // we got to wait
      WaitForServer(true);
    }
  } break;
  case CM_PAUSETIME: {
    ENABLE_FLAGS(m_iProtectedFlags, DMFC_PRF_PAUSETIME);
  } break;
  case CM_UNPAUSETIME: {
    DISABLE_FLAGS(m_iProtectedFlags, DMFC_PRF_PAUSETIME);
  } break;
  };
}

// DMFCBase::OnAllowObserverChange
//
//
//	returns true if the requested change for observer mode should go through
bool DMFCBase::OnAllowObserverChange(bool turnonobserver) { return true; }

// DMFCBase::OnClientShowUI
//
//
//	The game is saying it's ok to do any UI
void DMFCBase::OnClientShowUI(int id, void *user_data) {
  switch (id) {
  case UIID_TEAMPLACEMENT: {
    bool clients_wait;
    bool call_from_game;
    uint8_t d = *(uint8_t *)user_data;

    clients_wait = (d & 0x01) ? true : false;
    call_from_game = (d & 0x02) ? false : true;

    DoDMFCUITeamPlacement(clients_wait, call_from_game);
  } break;
  case UIID_WAITFORPLAYERS: {
    DoDMFCUIWaitForPlayers(true);
  } break;
  };
}

// DMFCBase::OnPrintScores
//
//
//	The user is requesting that the scores be printed out (For Dedicated Server use).  Use DPrintf
//	level:	-1	Requesting all the available scores
//			n	Print only the top n scores
//	Override this to how you see fit, but it should conform to the above.
void DMFCBase::OnPrintScores(int level) {}

// DMFCBase::OnGetHudCallSignColor
//
//	This is an event sent in by the game requesting what color it should draw the HUD callsign
//	of the passed in playernum.  Using GR_RGB return the color from the function.  This function
//	gets called every frame that the player is on the clients HUD.
ddgr_color DMFCBase::OnGetHudCallSignColor(int playernum) {
  if (!CheckPlayerNum(playernum))
    return GR_RGB(255, 255, 255);

  if (GetNumTeams() <= 1)
    return GR_RGB(0, 255, 0);

  return GetTeamColor(GetPlayerTeam(playernum));
}

// DMFCBase::OnTeamChangeName
//
//	This event occurs on the client when a team's name has just changed
void DMFCBase::OnTeamChangeName(int team, char *oldname, char *newname) {
  char names[DLLMAX_TEAMS][MAX_TEAMNAME_LEN];
  strcpy(names[RED_TEAM], GetTeamString(RED_TEAM));
  strcpy(names[BLUE_TEAM], GetTeamString(BLUE_TEAM));
  if (TeamMenuItem) {
    switch (m_iNumTeams) {
    case 2:
      TeamMenuItem->SetStateItemList(m_iNumTeams, names[RED_TEAM], names[BLUE_TEAM]);
      break;
    case 3:
      strcpy(names[GREEN_TEAM], GetTeamString(GREEN_TEAM));
      TeamMenuItem->SetStateItemList(m_iNumTeams, names[RED_TEAM], names[BLUE_TEAM], names[GREEN_TEAM]);
      break;
    case 4:
      strcpy(names[GREEN_TEAM], GetTeamString(GREEN_TEAM));
      strcpy(names[YELLOW_TEAM], GetTeamString(YELLOW_TEAM));
      TeamMenuItem->SetStateItemList(m_iNumTeams, names[RED_TEAM], names[BLUE_TEAM], names[GREEN_TEAM],
                                     names[YELLOW_TEAM]);
      break;
    }
  }
}

// DMFCBase::OnDoControls
//
//	This event occurs when there is a new back of game controls data
void DMFCBase::OnDoControls(game_controls *controls) {
  memcpy(&Last_game_controls, controls, sizeof(Last_game_controls));
}

// DMFCBase::OnClientWallCollide
//
//	Called by the game when their is a collision between an object and a wall
void DMFCBase::OnClientWallCollide(object *obj, float hitspeed, int hitseg, int hitwall, vector *hitpt,
                                   vector *wall_normal, float hit_dot) {}

//	DMFCBase::OnClientObjectKilled
//
//	Called when an object is being killed
//	Not automatically sent to clients (OnClientObjectKilled)
//	killer might not be valid (NULL)
void DMFCBase::OnClientObjectKilled(object *obj, object *killer) {}

//	DMFCBase::OnClientObjectDestroyed
//
//	Called when an object is about to be deleted
//	Not automatically sent to clients
void DMFCBase::OnClientObjectDestroyed(object *obj) {}

// DMFCBase::OnPlayAudioTaunt
//
//	This event occurs when a player plays an audio taunt
void DMFCBase::OnPlayAudioTaunt(int pnum) {
  mprintf(0, "%s plays an audio taunt\n", Players[pnum].callsign);
  SetPlayerTauntIndicator(pnum);
}

//	DMFCBase::OnGetTokenString
//
//	This event occurs when a text macro is being used that has tokens
//	in it, if you have a specific token, override this event to handle it.
void DMFCBase::OnGetTokenString(char *src, char *dest, int dest_size) {
  *dest = '\0';

  if (!stricmp(src, "e")) {
    // energy
    char buffer[64];
    int mypnum = GetPlayerNum();
    snprintf(buffer, sizeof(buffer), "%.0f", Players[mypnum].energy);
    strncpy(dest, buffer, dest_size - 1);
    dest[dest_size - 1] = '\0';
    return;
  }

  if (!stricmp(src, "s")) {
    // shields
    char buffer[64];
    int mypnum = GetPlayerNum();
    snprintf(buffer, sizeof(buffer), "%.0f", Objects[Players[mypnum].objnum].shields);
    strncpy(dest, buffer, dest_size - 1);
    dest[dest_size - 1] = '\0';
    return;
  }
}
