/*
* 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/gamecinematics.cpp $
 * $Revision: 49 $
 * $Date: 4/06/00 9:25a $
 * $Author: Matt $
 *
 * In-Game Cinematics system
 *
 * $Log: /DescentIII/main/gamecinematics.cpp $
 *
 * 49    4/06/00 9:25a Matt
 * Fixed a screen clear problem on at ATI Rage Fury Maxx in dual-chip mode
 * by forcing the screen to clear four times (instead of three).
 *
 * 48    10/08/99 4:14p 3dsmax
 * complex cinematic paths with only one node issue fixed.
 *
 * 47    5/22/99 12:07a Jeff
 * properly set the target handle when faking a canned cinematic
 *
 * 46    5/19/99 12:52p Samir
 * clear player weapon firing when entering cinematics,
 *
 * 45    5/07/99 7:48p Jeff
 * fixed demo recording during cinematics
 *
 * 44    5/07/99 1:44p Chris
 * Made in-game cinematics use the ORIENT_PATH_NODE for player ships
 *
 * 43    5/04/99 6:52p Jeff
 * added new canned cinematic to fade screen to white and endlevel.  Fixed
 * crash bug with invalid player path for canned cine with player paths
 *
 * 42    4/28/99 3:32a Jeff
 * created cinematic level reset function
 *
 * 41    4/27/99 5:58p Jeff
 * fixed for coop
 *
 * 40    4/27/99 5:41p Jeff
 * hopefully fixed coop/multiplayer issues
 *
 * 39    4/24/99 2:20a Chris
 * Added the Neutral_till_hit flag
 *
 * 38    4/15/99 11:40a Jeff
 * changes layout of igc intro
 *
 * 37    4/14/99 3:56a Jeff
 * fixed case mismatch in #includes
 *
 * 36    4/06/99 4:34p Jeff
 * fixed screenmode bug during endlevel
 *
 * 35    4/06/99 2:54p Jeff
 * fixed 1 node end level sequence path bug
 *
 * 34    4/02/99 11:33p Jeff
 * able to turn off IGC in debug mode.  Better handling of cinematics
 * quick exit if playing via alt-p
 *
 * 33    3/31/99 8:34p Jeff
 * different bitmap for trans
 *
 * 32    3/30/99 4:47p Jeff
 * added level events for when IGC occurs for a player
 *
 * 31    3/27/99 7:22p Jeff
 * fixed cinematics when going from one cut to another immediatly.  Added
 * start transition
 *
 * 30    3/19/99 9:39a Nate
 * Jeff - fixed crash bug
 *
 * 29    3/17/99 11:48a Jeff
 * intro cams are letterbox
 *
 * 28    3/10/99 6:44p Jeff
 * fixed exported game cinematic function for canned cinematics, so it's
 * prototype doesn't change (oem patch friendly)
 *
 * 27    3/10/99 6:20p Jeff
 * many fixes to demo system.  Fixed IGC so cameras move...fixed osiris to
 * be restored correctly, and it handles errors on restore
 *
 * 26    3/10/99 11:27a Jeff
 * fixed saving cinematic data when writing cinematics that use paths
 *
 * 25    3/08/99 3:24p Jeff
 * fixed end level sequencing where if the level ended while the player
 * was dead.
 *
 * 24    3/08/99 12:04p Jeff
 * fixed ship jitter in in-game cinematics (intro/end level)
 *
 * 23    3/04/99 3:57p Jeff
 * make player's burners on in intro/end level seqeunces if on a path
 *
 * 22    3/03/99 1:18p Jeff
 * turn off afterburner
 *
 * 21    3/01/99 5:35p Jeff
 * didn't change much
 *
 * 20    2/28/99 8:31p Jeff
 * added fade and move player dallas action.  Fixed the end-level sequence
 * changing view back to player for split second.
 *
 * 19    2/28/99 6:55p Jeff
 * orient the player correctly when 1 node path for level intro
 *
 * 18    2/26/99 6:38p Mark
 * fixed screen clearing problem
 *
 * 17    2/23/99 12:44a Jeff
 * added support for in-game-cinematics in demo system
 *
 * 16    2/22/99 1:20a Jeff
 * added support for inventory (simple) in dallas.  Moved end-level
 * sequence to use IGC.  Add position clipboard stuff for dallas.  Fixed
 * some inventory bug with storing object handles
 *
 * 15    2/21/99 8:35p Jeff
 * misc changes to handle new matcen and path types of dallas
 *
 * 14    2/20/99 4:15p Jeff
 * add cinematic text to game messages
 *
 * 13    2/18/99 6:47p Jeff
 * added call to start/stop special cinematics music
 *
 * 12    2/18/99 3:00p Jeff
 * smoother rotation/autolevel
 *
 * 11    2/16/99 9:36p Jeff
 * new add gamefile dialog
 *
 * 10    2/16/99 6:15p Jeff
 * pause hud messages when in cinematics
 *
 * 9     2/14/99 1:16a Jeff
 * added canned cinematic function/structures.  Added a flag to push
 * target to end of path if on a path (quick exit).  Added canned intro
 * cine.  Determine correct velocity on player ship for intro cine.
 *
 * 8     2/12/99 2:45a Jeff
 * added end-cinematic transitions
 *
 * 7     2/10/99 1:47p Matt
 * Changed object handle symbolic constants
 *
 * 6     2/04/99 3:56p Jeff
 * no cinematics in multiplayer
 *
 * 5     2/02/99 3:58p Jeff
 * started to implement level intro cinematic (need ai functions first).
 * No longer need camera object to do cinematic (auto-created)...path
 * cameras use speed based on distance needed to travel.
 *
 * 4     2/02/99 12:32p Jeff
 * added ai notify events for movie start and stop
 *
 * 3     2/01/99 12:55p Jeff
 * restore correct hud mode, added flag to stop cinematics if target dies
 *
 * 2     1/31/99 8:48p Jeff
 * new in game cinematics system finished
 *
 * $NoKeywords: $
 */

#include "pstypes.h"
#include "pserror.h"
#include "grtext.h"
#include "renderer.h"
#include "gamecinematics.h"
#include "vecmat.h"
#include "mem.h"
#include "gamepath.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "game.h"
#include "AIGoal.h"
#include "aipath.h"
#include "objinfo.h"
#include "player.h"
#include "ddio.h"
#include "gamefont.h"
#include "AIMain.h"
#include "controls.h"
#include "d3music.h"
#include "gamesequence.h"
#include "demofile.h"
#include "osiris_dll.h"
#include "multi.h"
#include "weapon.h"

#include <algorithm>

#if (defined(_DEBUG) || defined(EDITOR))
bool Cinematics_enabled = true;
extern int DoAI;
#ifdef EDITOR
extern bool Game_being_played_from_quick_play;
#endif
#endif
extern uint16_t Demo_obj_map[MAX_OBJECTS];

struct {
  // some important, pre-computed times
  float text_display_start, text_display_stop;
  float track_start, track_stop;
  float player_start, player_stop;
  float camera_start, camera_stop;
  float exit_start, exit_end;
  float end_time;

  void (*callback)(int type);

  int pathnum;
  int room;
  vector position;

  int target_objhandle;
  int camera_objhandle;

  int bmp_handle;
  int longest_line;

  uint32_t flags;

  tHUDMode old_hudmode;

  int num_lines;
  int end_transition_type;   // what kind of ending transition should the cinematic have
  int start_transition_type; // what kind of starting transition should the cinematic have
  char *text;
  bool target_triggered; // whether the notify event has been sent to the object yet

  bool doing_end_transition;       // we're processing an end transition still
  float end_transition_start_time; // Gametime that the end transition started
  float end_transition_end_time;   // Gametime that the end transition should end

  float start_transition_start_time;
  float start_transition_end_time;

  // some transition data
  float early_trans_end_start_time;

  bool controls_suspended;
} GameCinema;

#define CDI_NOT_CANNED 0
#define CDI_CANNED 1

struct tCinematicDemoInfo {
  uint8_t cinematic_type; // canned/not canned
  int camera_objhandle; // object handle for camera to use

  char *text_string; // not canned text string

  union {
    tCannedCinematicInfo *canned_data; // data to start a canned cinematic
    tGameCinematic *cinematic_data;    // data to start a regular cinematic
  };

};

static void Cinematic_WriteDemoFileData(tCinematicDemoInfo *info);
static bool Cinematic_StartCine(tGameCinematic *info, const char *text_string, int camera_objhandle);

static void Cinematic_DrawText(void);
static void Cinematic_DoEndTransition(void);
static void Cinematic_DoStartTransition(void);

bool Cinematic_inuse;
static tGameCinematic Cinematic_fake_info;
bool Cinematic_fake_queued = false;

static int gc_fade_bmp_handle = BAD_BITMAP_HANDLE;
static int gc_fadewhite_bmp_handle = BAD_BITMAP_HANDLE;
static int gc_wacky_bmp_handle = BAD_BITMAP_HANDLE;
static int gc_temp_bmp_handle = BAD_BITMAP_HANDLE;

#define END_TRANSITION_TIME 1.0f

static float Cine_GetPathTravelSpeed(int pathnum, float time);
static void Cinematic_PerformFake(void);
static void Cinematic_SetForFakeCinematic(tGameCinematic *info);
static inline void verify_percentranage(PercentageRange *range);
static inline int Cinematics_CreateCamera(void);
static inline void Cinematic_DeleteCamera(int objhandle);
static inline bool Cinematic_IsPlayerDead(void);
static bool Cinematic_IsKeyPressed(void);
static void CannedCinematicIntroCallback(int type);
static void CannedCinematic_Intro(int PathID, char *Text, int PlayerPath, float Seconds, int camera_handle);
static void CannedCinematicEndLevelCallback(int type);
static void CannedCinematic_EndLevelPath(int PathID, char *Text, int PlayerPath, float Seconds, int camera_handle);
static void CannedCinematic_EndLevelPoint(vector *pos, int room, char *Text, int PlayerPath, float Seconds,
                                          int camera_handle);
static void CannedCinematicMovePlayerFadeCallback(int type);
static void CannedCinematic_MovePlayerFade(object *player, int room, vector *pos, matrix *orient, int camera_handle);
static void CannedCinematicLevelEndFadeWhiteCallback(int type);
static void CannedCinematic_LevelEndFadeWhite(int camera_handle, float time, char *text_to_display);
static void Cinematic_DoFakeCannedCinematics(tCannedCinematicInfo *info);

//	Returns the hud mode before cinematics
tHUDMode Cinematic_GetOldHudMode(void) { return GameCinema.old_hudmode; }

//	Cinematic_Close
//
//	Closes the in-game cinematics
void Cinematic_Close(void) {
  if (Cinematic_inuse) {
    Cinematic_Stop();
  }

  if (gc_fade_bmp_handle > BAD_BITMAP_HANDLE) {
    bm_FreeBitmap(gc_fade_bmp_handle);
    gc_fade_bmp_handle = BAD_BITMAP_HANDLE;
  }

  if (gc_fadewhite_bmp_handle > BAD_BITMAP_HANDLE) {
    bm_FreeBitmap(gc_fadewhite_bmp_handle);
    gc_fadewhite_bmp_handle = BAD_BITMAP_HANDLE;
  }

  if (gc_temp_bmp_handle > BAD_BITMAP_HANDLE) {
    bm_FreeBitmap(gc_temp_bmp_handle);
    gc_temp_bmp_handle = BAD_BITMAP_HANDLE;
  }

  if (gc_wacky_bmp_handle > BAD_BITMAP_HANDLE) {
    bm_FreeBitmap(gc_wacky_bmp_handle);
    gc_wacky_bmp_handle = BAD_BITMAP_HANDLE;
  }

  GameCinema.doing_end_transition = false;
}

//	Cinematic_Init
//
//	Initializes the in-game cinematics
void Cinematic_Init(void) {
  static bool called_once = false;
  Cinematic_inuse = false;

  memset(&GameCinema, 0, sizeof(GameCinema));

  GameCinema.doing_end_transition = false;
  gc_fade_bmp_handle = BAD_BITMAP_HANDLE;

  gc_fade_bmp_handle = bm_AllocBitmap(32, 32, 0);
  if (gc_fade_bmp_handle > BAD_BITMAP_HANDLE) {
    uint16_t *data = bm_data(gc_fade_bmp_handle, 0);
    int i, size = 32 * 32;

    for (i = 0; i < size; i++)
      data[i] = GR_RGB16(0, 0, 0) | OPAQUE_FLAG;
  }

  gc_wacky_bmp_handle = bm_AllocLoadFileBitmap("ShieldBitmap.ogf", 0);

  gc_fadewhite_bmp_handle = bm_AllocBitmap(32, 32, 0);
  if (gc_fadewhite_bmp_handle > BAD_BITMAP_HANDLE) {
    uint16_t *data = bm_data(gc_fadewhite_bmp_handle, 0);
    int i, size = 32 * 32;

    for (i = 0; i < size; i++)
      data[i] = GR_RGB16(255, 255, 255) | OPAQUE_FLAG;
  }

  gc_temp_bmp_handle = bm_AllocBitmap(32, 32, 0);

  if (!called_once) {
    atexit(Cinematic_Close);
    called_once = true;
  }
}

void Cinematic_LevelInit(void) {
  Cinematic_inuse = false;
  Cinematic_fake_queued = false;
  GameCinema.doing_end_transition = false;
}

static inline void verify_percentranage(PercentageRange *range) {
  if (range->min < 0.0f)
    range->min = 0.0f;
  if (range->min > 1.0f)
    range->min = 1.0f;
  if (range->max < 0.0f)
    range->max = 0.0f;
  if (range->max > 1.0f)
    range->max = 1.0f;

  if (range->max < range->min) {
    float temp = range->max;
    range->max = range->min;
    range->min = temp;
  }
}

// Cinematic_PerformFake
//
//	Performs a fake cinematic...just calls certain functions that may need to be
//	called for certain things to happen.  For instance, some bosses are triggered when
//	their boss intro cam is started, but in multiplayer there is no cinematics, so calling
//	this would do what's needed.
void Cinematic_PerformFake(void) {
  Cinematic_fake_queued = false;
  mprintf(0, "Cinematics: Faking Cinematic\n");
  object *target = ObjGet(Cinematic_fake_info.target_objhandle);
  if (!target) {
    mprintf(0, "Cinematics: Invalid target...can't fake\n");
    return;
  }

  // tell the target it's the focus of a cinematic
  AINotify(target, AIN_MOVIE_START, NULL);
  AINotify(target, AIN_MOVIE_END, NULL);

  if (target->type == OBJ_PLAYER) {
    tOSIRISEventInfo ei;

    Osiris_CallLevelEvent(EVT_PLAYER_MOVIE_START, &ei);
    Osiris_CallLevelEvent(EVT_PLAYER_MOVIE_END, &ei);
  }
}

void Cinematic_SetForFakeCinematic(tGameCinematic *info) {
  ASSERT(Cinematic_fake_queued == false); // ack trying to play 2 cinematics in one frame!?

  memcpy(&Cinematic_fake_info, info, sizeof(tGameCinematic));
  Cinematic_fake_queued = true;
}

static inline int Cinematics_CreateCamera(void) {
  int objnum = ObjCreate(OBJ_CAMERA, 0, Player_object->roomnum, &Player_object->pos, NULL);
  if (objnum == -1)
    return OBJECT_HANDLE_NONE;

  if (Demo_flags == DF_RECORDING) {
    DemoWriteObjCreate(OBJ_CAMERA, 0, Player_object->roomnum, &Player_object->pos, NULL, OBJECT_HANDLE_NONE,
                       &Objects[objnum]);
  }

  return Objects[objnum].handle;
}

//	Cinematic_Start
//
//	Starts an in-game cinematic sequence.  text_string is the text to be displayed
//	use pipes (|) to seperate lines. (calls demo routines to record)
bool Cinematic_Start(tGameCinematic *info, char *text_string) {
  if (Demo_flags == DF_PLAYBACK)
    return true; // demo system calls Cinematic_StartCine itself

  ASSERT(info != NULL);
  ASSERT(text_string != NULL);
  if (!info || !text_string)
    return false;

  if (Cinematic_inuse) {
    Cinematic_SetForFakeCinematic(info);
    return false;
  }

  if (Game_mode & GM_MULTI) {
    Cinematic_SetForFakeCinematic(info);
    return false; // no cinematics in multiplayer
  }

  if ((info->flags & GCF_CAMERAPLACEMENT) == GCF_USEPATH) {
    info->orient = NULL; // ensure that this is NULL
  }

  int camera_handle = Cinematics_CreateCamera();

  if (Demo_flags == DF_RECORDING) {
    tCinematicDemoInfo demo_data;
    demo_data.cinematic_type = CDI_NOT_CANNED;
    demo_data.text_string = text_string;
    demo_data.cinematic_data = info;
    demo_data.camera_objhandle = camera_handle;
    Cinematic_WriteDemoFileData(&demo_data);
  }

  return Cinematic_StartCine(info, text_string, camera_handle);
}

static inline void Cinematic_DeleteCamera(int objhandle) {
  object *obj = ObjGet(objhandle);
  if (obj) {
    SetObjectDeadFlag(obj);
  }
}

//	Cinematic_StartCine
//
//	Starts an in-game cinematic sequence.  text_string is the text to be displayed
//	use pipes (|) to seperate lines. (does not call demo routines)
bool Cinematic_StartCine(tGameCinematic *info, const char *text_string, int camera_objhandle) {
  ASSERT(info != NULL);
  ASSERT(text_string != NULL);
  if (!info || !text_string) {
    Cinematic_DeleteCamera(camera_objhandle);
    return false;
  }

  if (Cinematic_inuse) {
    Cinematic_SetForFakeCinematic(info);
    Cinematic_DeleteCamera(camera_objhandle);
    return false;
  }

  if (Game_mode & GM_MULTI) {
    Cinematic_SetForFakeCinematic(info);
    Cinematic_DeleteCamera(camera_objhandle);
    return false; // no cinematics in multiplayer
  }

#ifdef _DEBUG
  if (!Cinematics_enabled) {
    Cinematic_SetForFakeCinematic(info);
    Cinematic_DeleteCamera(camera_objhandle);
    AddBlinkingHUDMessage("Cinematic: Not Displayed, they are disabled (DEL-SHIFT-C)");
    return true;
  }

  if (!DoAI) {
    Cinematic_SetForFakeCinematic(info);
    Cinematic_DeleteCamera(camera_objhandle);
    AddBlinkingHUDMessage("Cinematic: Not Displayed, AI is not turned on (DEL-A)");
    return true;
  }
#endif

  // take the given values and setup the struct
  memset(&GameCinema, 0, sizeof(GameCinema));

  // verify passed in values
  verify_percentranage(&info->text_display);
  verify_percentranage(&info->track_target);
  verify_percentranage(&info->player_disabled);
  verify_percentranage(&info->in_camera_view);
  verify_percentranage(&info->quick_exit);

  GameCinema.flags = info->flags;
  GameCinema.target_objhandle = info->target_objhandle;
  GameCinema.callback = info->callback;
  GameCinema.old_hudmode = GetHUDMode();

  int objnum;
  object *c = ObjGet(camera_objhandle);
  if (c) {
    objnum = OBJNUM(c);
  } else {
    mprintf(0, "Unable to create camera\n");
    return false;
  }

  if ((info->flags & GCF_CAMERAPLACEMENT) == GCF_USEPOINT) {
    GameCinema.room = info->room;
    GameCinema.position = info->position;
    // objnum = ObjCreate(OBJ_CAMERA,0,info->room,&info->position,info->orient);
    ObjSetPos(c, &info->position, info->room, info->orient, false);

  } else {
    GameCinema.pathnum = info->pathid;
    if (GameCinema.pathnum == -1) {
      mprintf(0, "Invalid Path given\n");
      return false;
    }
    // Abort if not a good path
    if (((info->flags & GCF_CAMERAPLACEMENT) == GCF_USEPOINT)) {
      if (GamePaths[GameCinema.pathnum].num_nodes < 2) {
        mprintf(0, "Not a good path passed to Cinematic_Start\n");
        return false;
      }
    }

    // objnum =
    // ObjCreate(OBJ_CAMERA,0,GamePaths[GameCinema.pathnum].pathnodes[0].roomnum,&GamePaths[GameCinema.pathnum].pathnodes[0].pos,NULL);
    ObjSetPos(c, &GamePaths[GameCinema.pathnum].pathnodes[0].pos, GamePaths[GameCinema.pathnum].pathnodes[0].roomnum,
              NULL, false);
  }

  /*
  if(objnum==-1){
          //couldn't create camera
          mprintf(0,"Unable to create camera\n");
          return false;
  }
  */

  GameCinema.camera_objhandle = Objects[objnum].handle;
  GameCinema.target_triggered = false;
  object *camera = &Objects[objnum];

  // parse the text line and extract the lines
  GameCinema.num_lines = 0;
  GameCinema.text = mem_strdup(text_string);
  char *ptr = GameCinema.text;
  if (!ptr) {
    Error("Out of Memory\n");
  }
  while (*ptr) {
    if (*ptr == '|') {
      *ptr = '\0';
      GameCinema.num_lines++;
    }
    ptr++;
  }
  GameCinema.num_lines++;

  float start_time = Gametime;
  GameCinema.end_time = start_time + info->max_time_play;
  GameCinema.text_display_start = start_time + (info->max_time_play * info->text_display.min);
  GameCinema.text_display_stop = start_time + (info->max_time_play * info->text_display.max);
  GameCinema.track_start = start_time + (info->max_time_play * info->track_target.min);
  GameCinema.track_stop = start_time + (info->max_time_play * info->track_target.max);
  GameCinema.player_start = start_time + (info->max_time_play * info->player_disabled.min);
  GameCinema.player_stop = start_time + (info->max_time_play * info->player_disabled.max);
  GameCinema.camera_start = start_time + (info->max_time_play * info->in_camera_view.min);
  GameCinema.camera_stop = start_time + (info->max_time_play * info->in_camera_view.max);
  GameCinema.exit_start = start_time + (info->max_time_play * info->quick_exit.min);
  GameCinema.exit_end = start_time + (info->max_time_play * info->quick_exit.max);
  GameCinema.start_transition_type = info->start_transition;
  GameCinema.end_transition_type = info->end_transition;
  GameCinema.doing_end_transition = false; // stop all pending end transitions
  GameCinema.controls_suspended = false;

  if (GameCinema.end_transition_type == GCTT_WACKY || GameCinema.end_transition_type == GCTT_FADEINOUT) {
    GameCinema.early_trans_end_start_time = start_time + info->max_time_play - (END_TRANSITION_TIME / 2.0f);
  }

  if (GameCinema.end_transition_type == GCTT_FADEWHITE) {
    GameCinema.early_trans_end_start_time = start_time;
  }

  if (GameCinema.start_transition_type == GCTT_NONE) {
    GameCinema.start_transition_start_time = start_time;
    GameCinema.start_transition_end_time = start_time;
  } else {
    GameCinema.start_transition_start_time = GameCinema.start_transition_end_time = -1.0f;
  }

  // The structs are setup, time to setup the camera and objects

  mprintf(0, "Cinematic_Start\n");

  Player_object->flags &= ~OF_DESTROYABLE;
  Players[Player_num].flags &= ~PLAYER_FLAGS_AFTERBURN_ON;
  camera->flags &= ~OF_DESTROYABLE;

  // setup camera physics
  camera->movement_type = MT_PHYSICS;
  camera->mtype.phys_info.flags &= ~PF_USES_THRUST;
  // camera->mtype.phys_info.flags |= PF_LEVELING;
  camera->mtype.phys_info.flags &= ~PF_LEVELING;
  camera->mtype.phys_info.drag = 0.1f;
  camera->mtype.phys_info.mass = 2.0f;
  camera->mtype.phys_info.rotdrag = 0.1f;

  vm_MakeZero(&camera->mtype.phys_info.thrust);
  vm_MakeZero(&camera->mtype.phys_info.rotthrust);
  vm_MakeZero(&camera->mtype.phys_info.rotvel);
  vm_MakeZero(&camera->mtype.phys_info.velocity);

  // setup camera AI
  SetObjectControlType(camera, CT_AI);
  memset(camera->ai_info, 0, sizeof(ai_frame));

  camera->ai_info->ai_class = AIC_STATIC;
  camera->ai_info->ai_type = AIT_AIS;

  GoalInitTypeGoals(camera, AIT_AIS);

  float velocity_to_use = 75.0f;

  if ((info->flags & GCF_CAMERAPLACEMENT) == GCF_USEPATH) {
    // determine velocity based on distance to travel on path
    velocity_to_use = Cine_GetPathTravelSpeed(GameCinema.pathnum, info->max_time_play);
  }

  camera->ai_info->flags =
      AIF_PERSISTANT | AIF_DISABLE_FIRING | AIF_DISABLE_MELEE | AIF_FORCE_AWARENESS | AIF_DETERMINE_TARGET;
  camera->ai_info->max_velocity = velocity_to_use; // Slightly faster than the real ship
  camera->ai_info->max_delta_velocity = 40.0f;
  camera->ai_info->max_turn_rate = 14000;
  camera->ai_info->awareness = AWARE_MOSTLY;
  camera->ai_info->movement_type = MC_FLYING;
  camera->ai_info->next_movement = AI_INVALID_INDEX;
  camera->ai_info->anim_sound_handle = 0;
  camera->ai_info->status_reg = 0;
  camera->ai_info->last_played_sound_index = -1;
  camera->ai_info->weapon_speed = 0.0f;
  camera->ai_info->animation_type = AS_ALERT;
  camera->ai_info->next_melee_time = Gametime;
  camera->ai_info->next_flinch_time = Gametime;
  camera->ai_info->target_handle = OBJECT_HANDLE_NONE;
  camera->ai_info->next_check_see_target_time = Gametime;
  camera->ai_info->last_see_target_time = Gametime;
  camera->ai_info->last_render_time = -1.0f;
  camera->ai_info->next_target_update_time = Gametime;
  camera->ai_info->notify_flags |= AI_NOTIFIES_ALWAYS_ON;
  camera->ai_info->last_see_target_pos = Zero_vector;
  camera->ai_info->dodge_vel_percent = 1.0f;
  camera->ai_info->attack_vel_percent = 1.0f;
  camera->ai_info->fight_same = 0.0f;
  camera->ai_info->agression = 0.0f;
  camera->ai_info->avoid_friends_distance = 0.0f;
  camera->ai_info->biased_flight_importance = 0.0f;
  camera->ai_info->circle_distance = 0.0f;
  camera->ai_info->dodge_percent = 0.0f;
  camera->ai_info->fight_team = 0.0;

  AIPathInitPath(&camera->ai_info->path);

  // Move camera to start position
  vector c_pos;
  int c_room;
  matrix c_orient;

  if ((info->flags & GCF_CAMERAPLACEMENT) == GCF_USEPOINT) {
    c_room = GameCinema.room;
    c_pos = GameCinema.position;
    if (info->orient)
      c_orient = *info->orient;
    else
      vm_MakeIdentity(&c_orient);
  } else {
    // Set up path info
    path_information pi;

    pi.path_id = GameCinema.pathnum;
    pi.start_node = 0;
    pi.end_node = GamePaths[GameCinema.pathnum].num_nodes - 1;

    if (GamePaths[GameCinema.pathnum].num_nodes == 1) {
      vm_VectorToMatrix(&c_orient, &GamePaths[GameCinema.pathnum].pathnodes[0].fvec,
                        &GamePaths[GameCinema.pathnum].pathnodes[0].uvec, NULL);
      pi.next_node = 0;
    } else {
      vector fvec = GamePaths[GameCinema.pathnum].pathnodes[1].pos - GamePaths[GameCinema.pathnum].pathnodes[0].pos;
      vm_VectorToMatrix(&c_orient, &fvec, NULL, NULL);
      pi.next_node = 1;
    }

    c_pos = GamePaths[GameCinema.pathnum].pathnodes[0].pos;
    c_room = GamePaths[GameCinema.pathnum].pathnodes[0].roomnum;

    // Make the player follow the path
    GoalAddGoal(camera, AIG_FOLLOW_PATH, (void *)&pi, 0, 10.0f, GF_ORIENT_TARGET | GF_FORCE_AWARENESS);
  }

  ObjSetPos(camera, &c_pos, c_room, &c_orient, true);

  // Switch to correct hud mode
  switch (GameCinema.flags & GCF_SCREENFORMAT) {
  case GCF_LETTERBOX:
    SetHUDMode(HUD_LETTERBOX);
    break;
  case GCF_FULLSCREEN:
    SetHUDMode(HUD_OBSERVER);
    break;
  }

  GameCinema.bmp_handle = -1;
  GameCinema.longest_line = 0;

  if ((GameCinema.flags & GCF_TEXT_MASK) == GCF_TEXT_WIPEIN) {

    // create overlay bitmap
    GameCinema.bmp_handle = bm_AllocBitmap(128, 128, 0);
    if (GameCinema.bmp_handle > BAD_BITMAP_HANDLE) {
      uint16_t *data = bm_data(GameCinema.bmp_handle, 0);
      for (int j = 0; j < 128 * 128; j++) {
        data[j] = GR_RGB16(0, 0, 0) | OPAQUE_FLAG;
      }
    }
  }

  // determine longest line and add text to game messages
  int old_font = grtext_GetFont();
  grtext_SetFont(BIG_BRIEFING_FONT);
  int pos = 0, len;
  char *str_ptr;

  for (int i = 0; i < GameCinema.num_lines; i++) {
    int l;

    len = strlen(&GameCinema.text[pos]);

    l = grtext_GetTextLineWidth(&GameCinema.text[pos]);
    if (GameCinema.longest_line < l)
      GameCinema.longest_line = l;

    // add text to game messages
    str_ptr = &GameCinema.text[pos];
    while (*str_ptr && (*str_ptr == ' ' || *str_ptr == '\t'))
      str_ptr++;
    if (*str_ptr)
      AddGameMessage(str_ptr);

    pos += len + 1;
  }
  GameCinema.longest_line += 10;
  grtext_SetFont(old_font);

  Cinematic_inuse = true;
  HUDPauseMessages();
  D3MusicStartCinematic();

  if (GameCinema.camera_start <= Gametime) {
    Viewer_object = camera;

    object *target;
    target = ObjGet(GameCinema.target_objhandle);

    if (GameCinema.track_start <= Gametime && target) {
      // face target immediatly
      vector u_axis;
      angle goal_angle;

      vector turn_to = target->pos - camera->pos;
      vm_NormalizeVector(&turn_to);

      if (turn_to == Zero_vector) {
        mprintf(0, "Cine: No turn_to or less than 1 degree off goal\n");
        goto continue_start;
      }

      goal_angle = vm_DeltaAngVecNorm(&camera->orient.fvec, &turn_to, &u_axis);
      if (goal_angle == 0) {
        mprintf(0, "Cine: Goal angle is zero\n");
        goto continue_start;
      }

      camera->orient.fvec = turn_to;

      camera->orient.uvec.x = camera->orient.uvec.z = 0.0f;
      camera->orient.uvec.y = 1.0f;

      vm_Orthogonalize(&camera->orient);
      ObjSetOrient(camera, &camera->orient);

    } else if ((GameCinema.flags & GCF_CAMERAPLACEMENT) == GCF_USEPATH) {
      // adjust to path node orientation (if we are on a path)
      int cur_path = camera->ai_info->path.cur_path;
      int cur_node = camera->ai_info->path.cur_node;
      int end_node = camera->ai_info->path.path_end_node[cur_path];
      int next_node = std::min(cur_node + 1, end_node);
      int path_num = camera->ai_info->path.path_id[cur_path];

      if (cur_node != next_node) {
        vector fvec = GamePaths[path_num].pathnodes[next_node].pos - GamePaths[path_num].pathnodes[cur_node].pos;
        vm_VectorToMatrix(&camera->orient, &fvec, NULL, NULL);
        vm_Orthogonalize(&camera->orient);
        ObjSetOrient(camera, &camera->orient);
      }
    }

  continue_start:;
  }

  if (GameCinema.callback)
    (*GameCinema.callback)(GCCT_START);

  return true;
}

static inline bool Cinematic_IsPlayerDead(void) {
  if (!Player_object || (Player_object->type == OBJ_GHOST) ||
      (Player_object->type == OBJ_PLAYER &&
       Players[Player_object->id].flags & (PLAYER_FLAGS_DYING | PLAYER_FLAGS_DEAD))) {
    return true;
  }
  return false;
}

//	Cinematic_Stop
//
//	Stops and clears up a in-game cinematic.
void Cinematic_Stop(void) {
  if (!Cinematic_inuse)
    return;

  if (GameCinema.text) {
    mem_free(GameCinema.text);
    GameCinema.text = NULL;
  }

  // tell the target object the cinematic is over
  object *target = ObjGet(GameCinema.target_objhandle);

  if (GameCinema.callback)
    (*GameCinema.callback)(GCCT_STOP);

  // reset things back to normal, anything that might be messed up
  bool dont_restore_viewer = false;
  if (GameCinema.flags & GCF_DONTRESTOREVIEWER) {
    mprintf(0, "********NOT RESTORING VIEW TO PLAYER*************\n");
    dont_restore_viewer = true;
  }

  if (!dont_restore_viewer) {
    Viewer_object = Player_object;
  }
  Players[Player_object->id].controller_bitflags = 0xFFFFFFFF;
  Player_object->flags |= OF_DESTROYABLE;
  ResumeControls();

  // go back to full screen
  if (!Cinematic_IsPlayerDead() && GetScreenMode() == SM_GAME) // player death cam has started
    SetHUDMode(Cinematic_GetOldHudMode());

  if (GameCinema.bmp_handle > BAD_BITMAP_HANDLE) {
    bm_FreeBitmap(GameCinema.bmp_handle);
  }

  // delete the camera
  if (!dont_restore_viewer) {
    object *cam = ObjGet(GameCinema.camera_objhandle);
    if (cam) {
      SetObjectDeadFlag(cam);
    }
  }

  Cinematic_inuse = false;
  HUDUnpauseMessages();
  D3MusicStopCinematic();

  // Start fade out transition
  if (GameCinema.end_transition_type == GCTT_FADE) {
    GameCinema.doing_end_transition = true;
    GameCinema.end_transition_start_time = Gametime;
    GameCinema.end_transition_end_time = Gametime + END_TRANSITION_TIME;
  } else if (GameCinema.end_transition_type == GCTT_WACKY) {
    // only start the wacky transition if it already hasn't (early break)
    if (!GameCinema.doing_end_transition) {
      GameCinema.doing_end_transition = true;
      GameCinema.end_transition_start_time = Gametime - (END_TRANSITION_TIME / 2);
      GameCinema.end_transition_end_time = Gametime + (END_TRANSITION_TIME / 2);
    }
  } else if (GameCinema.end_transition_type == GCTT_FADEINOUT) {
    // only start if it already hasn't
    if (!GameCinema.doing_end_transition) {
      GameCinema.doing_end_transition = true;
      GameCinema.end_transition_start_time = Gametime - (END_TRANSITION_TIME / 2);
      GameCinema.end_transition_end_time = Gametime + (END_TRANSITION_TIME / 2);
    }
  } else if (GameCinema.end_transition_type == GCTT_FADEWHITE) {
    if (!GameCinema.doing_end_transition) {
      GameCinema.doing_end_transition = true;
      GameCinema.end_transition_start_time = GameCinema.early_trans_end_start_time;
      GameCinema.end_transition_end_time = GameCinema.end_time;
    }
  }

  if (target) {
    AINotify(target, AIN_MOVIE_END, NULL);

    if (target->type == OBJ_PLAYER) {
      tOSIRISEventInfo ei;
      Osiris_CallLevelEvent(EVT_PLAYER_MOVIE_END, &ei);
    }
  }
}

bool Cinematic_IsKeyPressed(void) {
  for (int i = 0; i < DDIO_MAX_KEYS; i++) {
    if (ddio_InternalKeyState(i))
      return true;
  }

  return false;
}

//	Cinematic_Frame
//
//	Processes a frame for the Cinematics
void Cinematic_Frame(void) {
  if (Cinematic_fake_queued) {
    Cinematic_PerformFake();
  }

  if (!Cinematic_inuse) {
    return;
  }

  // check to see if the player died somehow
  if ((!(GameCinema.flags & GCF_IGNOREPLAYERDEAD)) && Cinematic_IsPlayerDead()) {
    Cinematic_Stop();
    return;
  }

  // check for end of cinematic
  if (Gametime > GameCinema.end_time) {
    Cinematic_Stop();
    return;
  }

  // check for keyboard quick exit
  if (Gametime >= GameCinema.exit_start && Gametime <= GameCinema.exit_end) {
    if (Cinematic_IsKeyPressed()) {
      // a key has been pressed, quick exit!
      ddio_KeyFlush();

      if (GameCinema.flags & GCF_FORCETARGETTOEND) {
        // move object to end of path
        object *target = ObjGet(GameCinema.target_objhandle);
        if (target && target->ai_info && target->ai_info->path.num_paths > 0) {
          bool shouldmove = true;

#if defined(_DEBUG) && defined(EDITOR)
          extern bool Game_being_played_from_quick_play;

          if (Game_being_played_from_quick_play && target->type == OBJ_PLAYER) {
            shouldmove = false;
          }
#endif

          // its a target on a path
          matrix orient;
          vector pos;
          int room;

          int currpath = target->ai_info->path.path_id[target->ai_info->path.cur_path];
          int endnode = GamePaths[currpath].num_nodes - 1;

          vm_VectorToMatrix(&orient, &GamePaths[currpath].pathnodes[endnode].fvec,
                            &GamePaths[currpath].pathnodes[endnode].uvec, NULL);

          pos = GamePaths[currpath].pathnodes[endnode].pos;
          room = GamePaths[currpath].pathnodes[endnode].roomnum;

          if (shouldmove) {
            ObjSetPos(target, &pos, room, &orient, true);
          }
        }
      }
      Cinematic_Stop();
      return;
    }
  }

  object *camera = ObjGet(GameCinema.camera_objhandle);
  object *target = ObjGet(GameCinema.target_objhandle);

  if (!target && GameCinema.flags & GCF_STOPIFTAGETDEAD) {
    Cinematic_Stop();
    return;
  }

  // tell the target it's the focus of a cinematic
  if (target && !GameCinema.target_triggered) {
    GameCinema.target_triggered = true;
    AINotify(target, AIN_MOVIE_START, NULL);

    if (target->type == OBJ_PLAYER) {
      tOSIRISEventInfo ei;
      Osiris_CallLevelEvent(EVT_PLAYER_MOVIE_START, &ei);
    }
  }

  // check to see if we should be looking through the camera
  if (Gametime >= GameCinema.camera_start && Gametime <= GameCinema.camera_stop && camera != NULL) {
    // camera should be the viewer
    Viewer_object = camera;
  } else {
    Viewer_object = Player_object;
  }

  // check to see if we should be disabling player controls
  if (Gametime >= GameCinema.player_start && Gametime <= GameCinema.player_stop) {
    // disable controls
    Players[Player_object->id].controller_bitflags = 0;

    // make sure play controls are suspended
    if (!GameCinema.controls_suspended) {
      SuspendControls();
      GameCinema.controls_suspended = true;
    }
  } else {
    // make sure controls are enabled
    Players[Player_object->id].controller_bitflags = 0xFFFFFFFF;

    // make sure play controls are enabled
    if (GameCinema.controls_suspended) {
      ResumeControls();
      GameCinema.controls_suspended = false;
    }
  }

  // adjust view to target
  if (Gametime >= GameCinema.track_start && Gametime <= GameCinema.track_stop && target) {
    // track target

    vector u_axis;
    angle goal_angle;

    vector turn_to = target->pos - camera->pos;
    vm_NormalizeVector(&turn_to);

    if (turn_to == Zero_vector) {
      mprintf(0, "Cine: No turn_to or less than 1 degree off goal\n");
      goto continue_frame;
    }

    goal_angle = vm_DeltaAngVecNorm(&camera->orient.fvec, &turn_to, &u_axis);
    if (goal_angle == 0) {
      mprintf(0, "Cine: Goal angle is zero\n");
      goto continue_frame;
    }

    camera->orient.fvec = turn_to;

    camera->orient.uvec.x = camera->orient.uvec.z = 0.0f;
    camera->orient.uvec.y = 1.0f;

    vm_Orthogonalize(&camera->orient);
    ObjSetOrient(camera, &camera->orient);

  } else if ((GameCinema.flags & GCF_CAMERAPLACEMENT) == GCF_USEPATH) {
    // adjust to path node orientation (if we are on a path)
    int cur_path = camera->ai_info->path.cur_path;
    int cur_node = camera->ai_info->path.cur_node;
    int end_node = camera->ai_info->path.path_end_node[cur_path];
    int next_node = std::min(cur_node + 1, end_node);
    int path_num = camera->ai_info->path.path_id[cur_path];

    if (cur_node != next_node) {
      vector fvec = GamePaths[path_num].pathnodes[next_node].pos - GamePaths[path_num].pathnodes[cur_node].pos;
      vm_VectorToMatrix(&camera->orient, &fvec, NULL, NULL);
      vm_Orthogonalize(&camera->orient);
      ObjSetOrient(camera, &camera->orient);
    }
  } else {
    // don't adjust at all
  }

continue_frame:

  if (GameCinema.end_transition_type == GCTT_WACKY || GameCinema.end_transition_type == GCTT_FADEINOUT) {
    if (Gametime > GameCinema.early_trans_end_start_time && !GameCinema.doing_end_transition) {
      // start the transition early
      GameCinema.doing_end_transition = true;
      GameCinema.end_transition_start_time = Gametime;
      GameCinema.end_transition_end_time = Gametime + END_TRANSITION_TIME;
    }
  }

  if (GameCinema.end_transition_type == GCTT_FADEWHITE) {
    if (Gametime > GameCinema.early_trans_end_start_time && !GameCinema.doing_end_transition) {
      // start the transition early
      GameCinema.doing_end_transition = true;
      GameCinema.end_transition_start_time = GameCinema.early_trans_end_start_time;
      GameCinema.end_transition_end_time = GameCinema.end_time;
    }
  }
}

// Renders anything that needs to be rendered for the cinematic frame
void Cinematic_RenderFrame(void) {
  if (!Cinematic_inuse) {
    if (GameCinema.doing_end_transition)
      Cinematic_DoEndTransition();
    return;
  }

  if (GetHUDMode() == HUD_LETTERBOX) {
    // clear letterbox areas
    Clear_screen = 4;
  }

  if (GameCinema.start_transition_type != GCTT_NONE) {
    Cinematic_DoStartTransition();
  }

  if (GameCinema.flags & GCF_DOTRANSBEFORETEXT) {
    // some end transitions start early...process now
    if (GameCinema.doing_end_transition)
      Cinematic_DoEndTransition();

    // see if we should be displaying text
    if (Gametime >= GameCinema.text_display_start && Gametime < GameCinema.text_display_stop) {
      // Display text
      Cinematic_DrawText();
    }

  } else {
    // see if we should be displaying text
    if (Gametime >= GameCinema.text_display_start && Gametime < GameCinema.text_display_stop) {
      // Display text
      Cinematic_DrawText();
    }

    // some end transitions start early...process now
    if (GameCinema.doing_end_transition)
      Cinematic_DoEndTransition();
  }

  if (GameCinema.callback)
    (*GameCinema.callback)(GCCT_FRAME);
}

void Cinematic_DrawText(void) {
  int font_height = grfont_GetHeight(BIG_BRIEFING_FONT) + 2;
  int old_font = grtext_GetFont();
  grtext_SetFont(BIG_BRIEFING_FONT);
  grtext_SetColor(GR_RGB(255, 255, 255));

  int start_y;
  int end_y;

  // determine y positions
  switch (GameCinema.flags & GCF_LAYOUT_MASK) {
  case GCF_LAYOUT_LOW: {
    end_y = 0.6667f * (float)(Max_window_h);
    start_y = end_y - (GameCinema.num_lines * font_height);

    ASSERT(start_y >= 0); // too many lines of text!

  } break;
  case GCF_LAYOUT_BOTTOM: {
    end_y = Max_window_h;
    start_y = end_y - (GameCinema.num_lines * font_height);

    ASSERT(start_y >= 0); // too many lines of text!
  } break;
  case GCF_LAYOUT_TOP: {
    start_y = 2;
    end_y = (GameCinema.num_lines * font_height) + start_y;

    ASSERT(end_y < Max_window_h); // too many lines of text!
  } break;
  case GCF_LAYOUT_MIDDLE: {
    int total_height = GameCinema.num_lines * font_height;
    start_y = (Max_window_h / 2) - (total_height / 2);
    end_y = start_y + total_height;

    ASSERT(start_y >= 0 && end_y < Max_window_h); // too many lines of text

  } break;
  }

  if ((GameCinema.flags & GCF_TEXT_MASK) == GCF_TEXT_WIPEIN) {
    // see if this mode is allowed (only if letterbox and not in middle layout)
    if ((GameCinema.flags & GCF_SCREENFORMAT) != GCF_LETTERBOX) {
      GameCinema.flags &= ~GCF_TEXT_WIPEIN;
      GameCinema.flags |= GCF_TEXT_FADEINOUT;
    } else {
      // it's letter box, make sure it's not middle
      if ((GameCinema.flags & GCF_LAYOUT_MASK) == GCF_LAYOUT_MIDDLE) {
        GameCinema.flags &= ~GCF_TEXT_WIPEIN;
        GameCinema.flags |= GCF_TEXT_FADEINOUT;
      }
      if ((GameCinema.flags & GCF_LAYOUT_MASK) == GCF_LAYOUT_LOW) {
        GameCinema.flags &= ~GCF_TEXT_WIPEIN;
        GameCinema.flags |= GCF_TEXT_FADEINOUT;
      }
    }
  }

  float total_text_time = GameCinema.text_display_stop - GameCinema.text_display_start;
  if (total_text_time <= 0.0f) {
    grtext_SetFont(old_font);
    return;
  }

  float time_in = Gametime - GameCinema.text_display_start;
  float perc = time_in / total_text_time;
  if (perc < 0.0f)
    perc = 0.0f;
  if (perc > 1.0f)
    perc = 1.0f;

  switch (GameCinema.flags & GCF_TEXT_MASK) {
  case GCF_TEXT_WIPEIN: {
    // first 1/3 is alpha in
    // last 2/3 is solid
    uint8_t alpha = 0;

    if (perc > 0.33f) {
      // middle 1/3
      alpha = 255;
    } else if (perc <= 0.33f) {
      // first 1/3
      float new_p = perc / 0.33f;
      alpha = (uint8_t)(new_p * 255.0f);
    }
    grtext_SetAlpha(alpha);

    int len, pos = 0;
    int y = start_y;
    for (int i = 0; i < GameCinema.num_lines; i++) {
      len = strlen(&GameCinema.text[pos]);
      grtext_CenteredPrintf(0, y, &GameCinema.text[pos]);

      y += font_height;
      pos += len + 1;
    }
    grtext_Flush();

    // Due alpha overlay effect
    if (GameCinema.bmp_handle > BAD_BITMAP_HANDLE) {
      float alphas[4] = {0.0f, 1.0f, 1.0f, 0.0f};
      float p = (perc > 0.18f) ? 1.0f : (perc / 0.18f);

      int gl, gr, gt, gb;
      grtext_GetParameters(&gl, &gt, &gr, &gb, NULL);

      int width = (1.0f - p) * GameCinema.longest_line;
      int rx = gl + (gr - gl) / 2 + (GameCinema.longest_line / 2);
      int lx = gl + rx - width;

      rend_SetAlphaType(AT_VERTEX);
      rend_SetLighting(LS_NONE);
      rend_SetColorModel(CM_MONO);
      rend_SetOverlayType(OT_NONE);
      rend_SetFiltering(0);
      rend_SetWrapType(WT_CLAMP);
      rend_SetZBufferState(0);

      int t_y, b_y;
      if ((GameCinema.flags & GCF_LAYOUT_MASK) == GCF_LAYOUT_TOP) {
        t_y = 0;
        b_y = Game_window_y;
      } else {
        t_y = Game_window_y + Game_window_h;
        b_y = Max_window_h;
      }

      rend_DrawScaledBitmap(lx, t_y, rx, b_y, GameCinema.bmp_handle, 0, 0, 1, 1, -1, alphas);

      rend_SetFiltering(1);
      rend_SetZBufferState(1);
    }
  } break;
  case GCF_TEXT_FADEINOUT: {
    // first 1/3 is alpha in
    // middle 1/3 is solid
    // last 1/3 is alpha out
    uint8_t alpha;

    if (perc > 0.33f && perc <= 0.66f) {
      // middle 1/3
      alpha = 255;
    } else if (perc <= 0.33f) {
      // first 1/3
      float new_p = perc / 0.33f;
      alpha = (uint8_t)(new_p * 255.0f);
    } else {
      // last 1/3
      float new_p = (perc - 0.66f) / 0.33f;
      if (new_p < 0.0f)
        new_p = 0.0f;
      if (new_p > 1.0f)
        new_p = 1.0f;
      alpha = (uint8_t)(255.0f - (new_p * 255.0f));
    }
    grtext_SetAlpha(alpha);

    int len, pos = 0;
    int y = start_y;
    for (int i = 0; i < GameCinema.num_lines; i++) {
      len = strlen(&GameCinema.text[pos]);
      grtext_CenteredPrintf(0, y, &GameCinema.text[pos]);

      y += font_height;
      pos += len + 1;
    }
    grtext_Flush();
  } break;
  case GCF_TEXT_NOEFFECT: {
    int len, pos = 0;
    int y = start_y;
    for (int i = 0; i < GameCinema.num_lines; i++) {
      len = strlen(&GameCinema.text[pos]);
      grtext_CenteredPrintf(0, y, &GameCinema.text[pos]);

      y += font_height;
      pos += len + 1;
    }
    grtext_Flush();
  } break;
  }

  grtext_SetFont(old_font);
}

void Cinematic_DoStartTransition(void) {
  if (GameCinema.start_transition_type == GCTT_NONE)
    return;

  if (GameCinema.start_transition_start_time == -1.0f) {
    // calculate
    GameCinema.start_transition_start_time = Gametime;
    GameCinema.start_transition_end_time = Gametime + 2.0f;
  }

  if (GameCinema.start_transition_end_time < Gametime)
    return;

  float total_time = GameCinema.start_transition_end_time - GameCinema.start_transition_start_time;
  float time_in = Gametime - GameCinema.start_transition_start_time;

  if (time_in < 0 || time_in > total_time)
    return;

  float perc = time_in / total_time;

  if (perc < 0.0f)
    perc = 0.0f;
  if (perc > 1.0f)
    perc = 1.0f;

  if (gc_fade_bmp_handle <= BAD_BITMAP_HANDLE || gc_fadewhite_bmp_handle <= BAD_BITMAP_HANDLE) {
    GameCinema.start_transition_type = GCTT_NONE;
    return;
  }

  float alpha = 1.0f - perc;

  rend_SetAlphaType(AT_CONSTANT);
  rend_SetAlphaValue(alpha * 255.0f);
  rend_SetLighting(LS_NONE);
  rend_SetColorModel(CM_MONO);
  rend_SetOverlayType(OT_NONE);
  rend_SetWrapType(WT_CLAMP);
  rend_SetFiltering(0);
  rend_SetZBufferState(0);

  rend_DrawScaledBitmap(
      0, 0, Max_window_w, Max_window_h,
      (GameCinema.start_transition_type == GCTT_FADEWHITE) ? gc_fadewhite_bmp_handle : gc_fade_bmp_handle, 0, 0, 1, 1);

  rend_SetFiltering(1);
  rend_SetZBufferState(1);
}

void Cinematic_DoEndTransition(void) {
  if (!GameCinema.doing_end_transition)
    return;

  if (Gametime > GameCinema.end_transition_end_time) {
    GameCinema.doing_end_transition = false;
    return;
  }

  if (Gametime < GameCinema.end_transition_start_time) {
    GameCinema.doing_end_transition = false;
    return;
  }

  float time_in = Gametime - GameCinema.end_transition_start_time;
  float time_full = GameCinema.end_transition_end_time - GameCinema.end_transition_start_time;
  float perc = time_in / time_full;

  if (perc < 0.0f)
    perc = 0.0f;
  if (perc > 1.0f)
    perc = 1.0f;

  switch (GameCinema.end_transition_type) {
  case GCTT_WACKY: {
    if (gc_wacky_bmp_handle <= BAD_BITMAP_HANDLE) {
      GameCinema.doing_end_transition = false;
      return;
    }
    int w, h;
    w = bm_w(gc_wacky_bmp_handle, 0);
    h = bm_h(gc_wacky_bmp_handle, 0);

    float wscale, hscale;
    wscale = (float)(Max_window_w) / (float)(w);
    hscale = (float)(Max_window_h) / (float)(h);

    if (wscale < hscale) {
      float whratio = (float)(w) / (float)(h);

      // width is bigger
      h = Max_window_h * whratio;
      w = Max_window_w;
    } else {
      float hwratio = (float)(h) / (float)(w);

      // height is bigger
      h = Max_window_h;
      w = Max_window_w * hwratio;
    }

    float ratio = 0.0f;

    if (perc < 0.5f) {
      // small->large
      ratio = perc / 0.5f;
    } else {
      // large->small
      ratio = 1.0f - ((perc - 0.5f) / 0.5f);
    }

    w *= ratio;
    h *= ratio;

    int x, y;

    x = (Max_window_w / 2) - (w / 2);
    y = (Max_window_h / 2) - (h / 2);

    rend_SetAlphaType(AT_TEXTURE);
    rend_SetLighting(LS_NONE);
    rend_SetColorModel(CM_MONO);
    rend_SetOverlayType(OT_NONE);
    rend_SetWrapType(WT_CLAMP);
    rend_SetFiltering(0);
    rend_SetZBufferState(0);

    rend_DrawScaledBitmap(x, y, x + w, y + h, gc_wacky_bmp_handle, 0, 0, 1, 1);

    rend_SetFiltering(1);
    rend_SetZBufferState(1);

  } break;

  case GCTT_FADE: {
    if (gc_fade_bmp_handle <= BAD_BITMAP_HANDLE) {
      GameCinema.doing_end_transition = false;
      return;
    }

    float alpha = 1.0f - perc;

    rend_SetAlphaType(AT_CONSTANT);
    rend_SetAlphaValue(alpha * 255.0f);
    rend_SetLighting(LS_NONE);
    rend_SetColorModel(CM_MONO);
    rend_SetOverlayType(OT_NONE);
    rend_SetWrapType(WT_CLAMP);
    rend_SetFiltering(0);
    rend_SetZBufferState(0);

    rend_DrawScaledBitmap(0, 0, Max_window_w, Max_window_h, gc_fade_bmp_handle, 0, 0, 1, 1);

    rend_SetFiltering(1);
    rend_SetZBufferState(1);

  } break;

  case GCTT_FADEINOUT: {
    if (gc_fade_bmp_handle <= BAD_BITMAP_HANDLE) {
      GameCinema.doing_end_transition = false;
      return;
    }

    float alpha = 0.0f;

    if (perc < 0.5f) {
      // transparent->opaque
      alpha = perc / 0.5f;
    } else {
      // opaque->transparent
      alpha = 1.0f - ((perc - 0.5f) / 0.5f);
    }

    rend_SetAlphaType(AT_CONSTANT);
    rend_SetAlphaValue(alpha * 255.0f);
    rend_SetLighting(LS_NONE);
    rend_SetColorModel(CM_MONO);
    rend_SetOverlayType(OT_NONE);
    rend_SetWrapType(WT_CLAMP);
    rend_SetFiltering(0);
    rend_SetZBufferState(0);

    rend_DrawScaledBitmap(0, 0, Max_window_w, Max_window_h, gc_fade_bmp_handle, 0, 0, 1, 1);

    rend_SetFiltering(1);
    rend_SetZBufferState(1);

  } break;
  case GCTT_FADEWHITE: {
    if (gc_fade_bmp_handle <= BAD_BITMAP_HANDLE) {
      GameCinema.doing_end_transition = false;
      return;
    }

#define CUTOFF 0.8f

    float alpha;
    bool use_white;
    if (perc < CUTOFF) {
      // to white
      alpha = perc / CUTOFF;
      use_white = true;
    } else {
      // to black
      alpha = 1.0f;
      use_white = false;
    }

    rend_SetAlphaType(AT_CONSTANT);
    rend_SetAlphaValue(alpha * 255.0f);
    rend_SetLighting(LS_NONE);
    rend_SetColorModel(CM_MONO);
    rend_SetOverlayType(OT_NONE);
    rend_SetWrapType(WT_CLAMP);
    rend_SetFiltering(0);
    rend_SetZBufferState(0);

    if (use_white) {
      rend_DrawScaledBitmap(0, 0, Max_window_w, Max_window_h, gc_fadewhite_bmp_handle, 0, 0, 1, 1);
    } else {
      if (gc_temp_bmp_handle > BAD_BITMAP_HANDLE) {
        float fade_out_perc = 1.0f - ((perc - CUTOFF) / (1.0f - CUTOFF));
        int grey = 255.0f * fade_out_perc;
        uint16_t *data = bm_data(gc_temp_bmp_handle, 0);
        int i, size = 32 * 32;

        for (i = 0; i < size; i++)
          data[i] = GR_RGB16(grey, grey, grey) | OPAQUE_FLAG;

        GameBitmaps[gc_temp_bmp_handle].flags |= BF_CHANGED;

        rend_DrawScaledBitmap(0, 0, Max_window_w, Max_window_h, gc_temp_bmp_handle, 0, 0, 1, 1);
      }
    }

    rend_SetFiltering(1);
    rend_SetZBufferState(1);

  } break;

  case GCTT_NONE:
    GameCinema.doing_end_transition = false;
    return;
    break;
  }
}

float Cine_GetPathTravelSpeed(int pathnum, float time) {
  float velocity_to_use = 75.0f;

  // determine velocity based on distance to travel on path
  int num_nodes = GamePaths[pathnum].num_nodes;
  float distance;
  distance = 0;

  for (int n = 0; n < num_nodes - 1; n++) {
    distance += vm_VectorDistance(&GamePaths[pathnum].pathnodes[n].pos, &GamePaths[pathnum].pathnodes[n + 1].pos);
  }

  if (time > 0)
    velocity_to_use = distance / time;

  return velocity_to_use;
}

/*
***********************************************************************
        Intro Movie Canned Cinematic
***********************************************************************
*/

struct {
  int player_path;
  float cinematic_time;
  bool should_thrust;
} CannedCinematicIntro;

void CannedCinematicIntroCallback(int type) {
  switch (type) {
  case GCCT_START: {
    // start the player on the path
    mprintf(0, "START PLAYER ON PATH\n");

    // determine how fast they need to travel
    int pathnum = CannedCinematicIntro.player_path;
    float velocity = Cine_GetPathTravelSpeed(pathnum, CannedCinematicIntro.cinematic_time);

    SuspendControls();

    if (Players[Player_num].objnum > -1) {
      object *playerobj = &Objects[Players[Player_num].objnum];
      ClearPlayerFiring(playerobj, PW_PRIMARY);
      ClearPlayerFiring(playerobj, PW_SECONDARY);
    }

    PlayerSetControlToAI(Player_num, velocity);

    // move the target (player) to the first node of the path
    // this makes it multiplayer friendly since this will only be called
    // in a single player game
    vector pos;
    matrix orient;
    int roomnum;
    int next_node;

    if (pathnum == -1) {
      mprintf(0, "ILLEGAL PATH FOR PLAYER\n");
      break;
    }

    if (GamePaths[pathnum].num_nodes > 1) {
      next_node = 1;
      vector fvec = GamePaths[pathnum].pathnodes[1].pos - GamePaths[pathnum].pathnodes[0].pos;
      vm_VectorToMatrix(&orient, &fvec, NULL, NULL);

      // see if it is a hacked 2 node path (i.e. its such a small distance he should move
      if (GamePaths[pathnum].num_nodes == 2) {
        if (fabs(vm_VectorDistance(&GamePaths[pathnum].pathnodes[1].pos, &GamePaths[pathnum].pathnodes[0].pos)) >
            30.0f) {
          CannedCinematicIntro.should_thrust = true;
          mprintf(0, "Player should thrust\n");
        } else {
          CannedCinematicIntro.should_thrust = false;
          mprintf(0, "Player should NOT thrust\n");
        }
      } else {
        CannedCinematicIntro.should_thrust = true;
        mprintf(0, "Player should thrust\n");
      }
    } else {
      next_node = 0;
      CannedCinematicIntro.should_thrust = false;
      mprintf(0, "Player should NOT thrust\n");
      vm_VectorToMatrix(&orient, &GamePaths[pathnum].pathnodes[0].fvec, &GamePaths[pathnum].pathnodes[0].uvec, NULL);
    }
    pos = GamePaths[pathnum].pathnodes[0].pos;
    roomnum = GamePaths[pathnum].pathnodes[0].roomnum;

    ObjSetPos(Player_object, &pos, roomnum, &orient, true);

    if (CannedCinematicIntro.should_thrust) {
      // Set up path info
      path_information pi;
      pi.path_id = pathnum;
      pi.start_node = 0;
      pi.next_node = next_node;
      pi.end_node = GamePaths[pathnum].num_nodes - 1;

      // Make the player follow the path
      GoalAddGoal(Player_object, AIG_FOLLOW_PATH, (void *)&pi, 0, 1.0f, GF_ORIENT_PATH_NODE | GF_FORCE_AWARENESS);
    }
  } break;
  case GCCT_STOP: {
    // stop the player on the path
    mprintf(0, "STOP PLAYER ON PATH\n");
    ResetPlayerControlType(Player_num);
    ResumeControls();

    vm_MakeZero(&Player_object->mtype.phys_info.velocity);

    Players[Player_num].flags &= ~PLAYER_FLAGS_THRUSTED;

  } break;
  case GCCT_FRAME: {
    if (CannedCinematicIntro.should_thrust)
      Players[Player_num].flags |= PLAYER_FLAGS_THRUSTED;
    else
      Players[Player_num].flags &= ~PLAYER_FLAGS_THRUSTED;
  } break;
  }
}

void CannedCinematic_Intro(int PathID, const char *Text, int PlayerPath, float Seconds, int camera_handle) {
  CannedCinematicIntro.player_path = PlayerPath;
  CannedCinematicIntro.cinematic_time = Seconds;

  tGameCinematic info;
  info.flags = 0;
  info.flags = (GCF_LETTERBOX | GCF_USEPATH | GCF_TEXT_FADEINOUT | GCF_LAYOUT_BOTTOM | GCF_FORCETARGETTOEND);
  info.target_objhandle = Player_object->handle;
  info.max_time_play = Seconds;
  info.callback = CannedCinematicIntroCallback;
  info.text_display.min = 0.40f;
  info.text_display.max = 0.80f;
  info.track_target.min = 0.0f;
  info.track_target.max = 1.0f;
  info.player_disabled.min = 0.0f;
  info.player_disabled.max = 1.0f;
  info.in_camera_view.min = 0.0f;
  info.in_camera_view.max = 1.0f;
  info.quick_exit.min = 0.25f;
  info.quick_exit.max = 1.0f;

  info.end_transition = GCTT_FADEINOUT;
  info.start_transition = GCTT_FADE;
  info.pathid = PathID;

  Cinematic_StartCine(&info, Text, camera_handle);
}

struct {
  int player_path;
  float cinematic_time;
  bool should_thrust;
} CannedCinematicEndLevel;
extern bool EndLevel();

void CannedCinematicEndLevelCallback(int type) {
  switch (type) {
  case GCCT_START: {
    // if the player is dead, bring them back from the dead
    if ((Players[Player_num].flags & PLAYER_FLAGS_DYING) || (Players[Player_num].flags & PLAYER_FLAGS_DEAD)) {
      // the player is currently dead
      mprintf(0, "Bringing player back from the dead\n");
      Players[Player_num].flags &= ~(PLAYER_FLAGS_DYING | PLAYER_FLAGS_DEAD);
      Player_object->shields = 1.0f;
    }

    // Make player invulnerable
    MakePlayerInvulnerable(Player_num, CannedCinematicEndLevel.cinematic_time, false);

    // start the player on the path
    mprintf(0, "START PLAYER ON PATH\n");

    // determine how fast they need to travel
    int pathnum = CannedCinematicEndLevel.player_path;

    if (pathnum == -1) {
      mprintf(0, "ILLEGAL PATH FOR PLAYER\n");
      break;
    }

    float velocity = Cine_GetPathTravelSpeed(pathnum, CannedCinematicEndLevel.cinematic_time);

    SuspendControls();
    if (Players[Player_num].objnum > -1) {
      object *playerobj = &Objects[Players[Player_num].objnum];
      ClearPlayerFiring(playerobj, PW_PRIMARY);
      ClearPlayerFiring(playerobj, PW_SECONDARY);
    }

    PlayerSetControlToAI(Player_num, velocity);

    // move the target (player) to the first node of the path
    // this makes it multiplayer friendly since this will only be called
    // in a single player game
    vector pos;
    matrix orient;
    int roomnum;
    int next_node;

    if (GamePaths[pathnum].num_nodes > 1) {
      next_node = 1;
      vector fvec = GamePaths[pathnum].pathnodes[1].pos - GamePaths[pathnum].pathnodes[0].pos;
      vm_VectorToMatrix(&orient, &fvec, NULL, NULL);

      if (GamePaths[pathnum].num_nodes == 2) {
        if (fabs(vm_VectorDistance(&GamePaths[pathnum].pathnodes[1].pos, &GamePaths[pathnum].pathnodes[0].pos)) >
            30.0f) {
          CannedCinematicEndLevel.should_thrust = true;
        } else {
          CannedCinematicEndLevel.should_thrust = false;
        }
      } else {
        CannedCinematicEndLevel.should_thrust = true;
      }
    } else {
      next_node = 0;
      vm_VectorToMatrix(&orient, &GamePaths[pathnum].pathnodes[0].fvec, &GamePaths[pathnum].pathnodes[0].uvec, NULL);
      CannedCinematicEndLevel.should_thrust = false;
    }
    pos = GamePaths[pathnum].pathnodes[0].pos;
    roomnum = GamePaths[pathnum].pathnodes[0].roomnum;

    ObjSetPos(Player_object, &pos, roomnum, &orient, true);

    if (CannedCinematicEndLevel.should_thrust) {

      // Set up path info
      path_information pi;
      pi.path_id = pathnum;
      pi.start_node = 0;
      pi.next_node = next_node;
      pi.end_node = GamePaths[pathnum].num_nodes - 1;

      // Make the player follow the path
      GoalAddGoal(Player_object, AIG_FOLLOW_PATH, (void *)&pi, 0, 1.0f, GF_ORIENT_PATH_NODE | GF_FORCE_AWARENESS);
    }
  } break;
  case GCCT_STOP: {
    // stop the player on the path
    mprintf(0, "STOP PLAYER ON PATH\n");
    ResetPlayerControlType(Player_num);
    ResumeControls();

    vm_MakeZero(&Player_object->mtype.phys_info.velocity);

    SetGameState(GAMESTATE_LVLEND);
  } break;
  case GCCT_FRAME: {
    if (CannedCinematicEndLevel.should_thrust)
      Players[Player_num].flags |= PLAYER_FLAGS_THRUSTED;
    else
      Players[Player_num].flags &= ~PLAYER_FLAGS_THRUSTED;
  } break;
  }
}

void CannedCinematic_EndLevelPath(int PathID, const char *Text, int PlayerPath, float Seconds, int camera_handle) {
  CannedCinematicEndLevel.player_path = PlayerPath;
  CannedCinematicEndLevel.cinematic_time = Seconds;

  tGameCinematic info;
  info.flags = 0;
  info.flags = (GCF_LETTERBOX | GCF_USEPATH | GCF_TEXT_WIPEIN | GCF_LAYOUT_BOTTOM | GCF_FORCETARGETTOEND |
                GCF_DONTRESTOREVIEWER);
  info.target_objhandle = Player_object->handle;
  info.max_time_play = Seconds;
  info.callback = CannedCinematicEndLevelCallback;
  info.text_display.min = 0.40f;
  info.text_display.max = 0.80f;
  info.track_target.min = 0.0f;
  info.track_target.max = 1.0f;
  info.player_disabled.min = 0.0f;
  info.player_disabled.max = 1.0f;
  info.in_camera_view.min = 0.0f;
  info.in_camera_view.max = 1.0f;
  info.quick_exit.min = 1.0f;
  info.quick_exit.max = 1.0f;

  info.end_transition = GCTT_FADEINOUT;
  info.start_transition = GCTT_FADE;
  info.pathid = PathID;

  Cinematic_StartCine(&info, Text, camera_handle);
}

void CannedCinematic_EndLevelPoint(vector *pos, int room, const char *Text, int PlayerPath, float Seconds,
                                   int camera_handle) {
  CannedCinematicEndLevel.player_path = PlayerPath;
  CannedCinematicEndLevel.cinematic_time = Seconds;

  tGameCinematic info;
  info.flags = 0;
  info.flags = (GCF_LETTERBOX | GCF_USEPOINT | GCF_TEXT_WIPEIN | GCF_LAYOUT_BOTTOM | GCF_FORCETARGETTOEND |
                GCF_DONTRESTOREVIEWER);
  info.target_objhandle = Player_object->handle;
  info.max_time_play = Seconds;
  info.callback = CannedCinematicEndLevelCallback;
  info.text_display.min = 0.40f;
  info.text_display.max = 0.80f;
  info.track_target.min = 0.0f;
  info.track_target.max = 1.0f;
  info.player_disabled.min = 0.0f;
  info.player_disabled.max = 1.0f;
  info.in_camera_view.min = 0.0f;
  info.in_camera_view.max = 1.0f;
  info.quick_exit.min = 1.0f;
  info.quick_exit.max = 1.0f;
  info.end_transition = GCTT_FADEINOUT;
  info.start_transition = GCTT_FADE;

  info.position = *pos;
  info.room = room;
  info.orient = NULL;

  Cinematic_StartCine(&info, Text, camera_handle);
}

struct {
  object *player;
  int room;
  vector pos;
  matrix orient;
} CannedCinematicMovePlayerFade;

void CannedCinematicMovePlayerFadeCallback(int type) {
  switch (type) {
  case GCCT_FRAME:
    object *player = CannedCinematicMovePlayerFade.player;
    ObjSetPos(player, &CannedCinematicMovePlayerFade.pos, CannedCinematicMovePlayerFade.room,
              &CannedCinematicMovePlayerFade.orient, true);
    break;
  }
}

void CannedCinematic_MovePlayerFade(object *player, int room, vector *pos, matrix *orient, int camera_handle) {
  CannedCinematicMovePlayerFade.player = player;
  CannedCinematicMovePlayerFade.room = room;
  CannedCinematicMovePlayerFade.pos = *pos;
  CannedCinematicMovePlayerFade.orient = *orient;

  tGameCinematic info;
  info.flags = 0;
  info.flags = (GCF_FULLSCREEN | GCF_USEPOINT | GCF_TEXT_NOEFFECT | GCF_LAYOUT_BOTTOM);
  info.target_objhandle = OBJECT_HANDLE_NONE;
  info.max_time_play = 1.0f;
  info.callback = CannedCinematicMovePlayerFadeCallback;
  info.text_display.min = 1.0f;
  info.text_display.max = 1.0f;
  info.track_target.min = 1.0f;
  info.track_target.max = 1.0f;
  info.player_disabled.min = 1.0f;
  info.player_disabled.max = 1.0f;
  info.in_camera_view.min = 0.0f;
  info.in_camera_view.max = 1.0f;
  info.quick_exit.min = 1.0f;
  info.quick_exit.max = 1.0f;
  info.end_transition = GCTT_FADEINOUT;
  info.start_transition = GCTT_NONE;

  info.position = player->pos;
  info.room = player->roomnum;
  info.orient = &player->orient;

  Cinematic_StartCine(&info, "", camera_handle);
}

struct {
  float start_time;
  float max_time;
} CannedCinematicLevelEndFadeWhite;

void CannedCinematicLevelEndFadeWhiteCallback(int type) {
  switch (type) {
  case GCCT_START:
    break;
  case GCCT_FRAME:
    break;
  case GCCT_STOP:
    SetGameState(GAMESTATE_LVLEND);
    break;
  }
}

void CannedCinematic_LevelEndFadeWhite(int camera_handle, float time, const char *text_to_display) {
  CannedCinematicLevelEndFadeWhite.start_time = Gametime;
  CannedCinematicLevelEndFadeWhite.max_time = time;

  tGameCinematic info;
  info.flags = (GCF_LETTERBOX | GCF_USEPOINT | GCF_TEXT_NOEFFECT | GCF_LAYOUT_MIDDLE | GCF_DONTRESTOREVIEWER |
                GCF_IGNOREPLAYERDEAD | GCF_DOTRANSBEFORETEXT);
  info.target_objhandle = OBJECT_HANDLE_NONE;
  info.max_time_play = time;
  info.callback = CannedCinematicLevelEndFadeWhiteCallback;
  info.text_display.min = 0.0f;
  info.text_display.max = 1.0f;
  info.track_target.min = 1.0f;
  info.track_target.max = 1.0f;
  info.player_disabled.min = 1.0f;
  info.player_disabled.max = 1.0f;
  info.in_camera_view.min = 1.0f;
  info.in_camera_view.max = 1.0f;
  info.quick_exit.min = 1.0f;
  info.quick_exit.max = 1.0f;
  info.end_transition = GCTT_FADEWHITE;
  info.start_transition = GCTT_NONE;

  info.position = Player_object->pos;
  info.room = Player_object->roomnum;
  info.orient = &Player_object->orient;

  Cinematic_StartCine(&info, (text_to_display) ? text_to_display : "", camera_handle);
}

//==============================================================================
// Canned Cinematic interface

void Cinematic_DoFakeCannedCinematics(tCannedCinematicInfo *info) {
  switch (info->type) {
  case CANNED_LEVEL_INTRO:
    break;
  case CANNED_LEVEL_END_PATH:
  case CANNED_LEVEL_END_POINT:
  case CANNED_LEVEL_END_FADE_WHITE:
    if (Game_mode & GM_MULTI) {
      MultiEndLevel();
    } else {
      SetGameState(GAMESTATE_LVLEND);
    }
    break;
  case CANNED_MOVE_PLAYER_FADE:
    break;
  }
}

void Cinematic_StartCannedScript(tCannedCinematicInfo *info) {
  if (Demo_flags == DF_PLAYBACK)
    return; // demo system calls Cinematic_StartCanned itself

  Cinematic_StartCanned(info);
}

void Cinematic_StartCanned(tCannedCinematicInfo *info, int camera_handle) {
  if (Cinematic_inuse) {
    tGameCinematic gc;
    memset(&gc, 0, sizeof(tGameCinematic));
    switch (info->type) {
    case CANNED_LEVEL_INTRO:
    case CANNED_LEVEL_END_PATH:
    case CANNED_LEVEL_END_POINT:
    case CANNED_MOVE_PLAYER_FADE:
    case CANNED_LEVEL_END_FADE_WHITE:
      gc.target_objhandle = Player_object->handle;
      break;
    default: {
      gc.target_objhandle = OBJECT_HANDLE_NONE;
    } break;
    }

    Cinematic_SetForFakeCinematic(&gc);
    Cinematic_DoFakeCannedCinematics(info);
    if (camera_handle != -1)
      Cinematic_DeleteCamera(camera_handle);
    return;
  }

  if (Game_mode & GM_MULTI) {
    tGameCinematic gc;
    memset(&gc, 0, sizeof(tGameCinematic));
    switch (info->type) {
    case CANNED_LEVEL_INTRO:
    case CANNED_LEVEL_END_PATH:
    case CANNED_LEVEL_END_POINT:
    case CANNED_MOVE_PLAYER_FADE:
    case CANNED_LEVEL_END_FADE_WHITE:
      gc.target_objhandle = Player_object->handle;
      break;
    default: {
      gc.target_objhandle = OBJECT_HANDLE_NONE;
    } break;
    }

    Cinematic_SetForFakeCinematic(&gc);
    Cinematic_DoFakeCannedCinematics(info);
    if (camera_handle != -1)
      Cinematic_DeleteCamera(camera_handle);
    return; // no cinematics in multiplayer
  }

  if (camera_handle == -1) {
    // when playing back a demo, this should have a value
    ASSERT(Demo_flags != DF_PLAYBACK);
    camera_handle = Cinematics_CreateCamera();
  }

  if (Demo_flags == DF_RECORDING) {
    tCinematicDemoInfo demo_data;
    demo_data.cinematic_type = CDI_CANNED;
    demo_data.text_string = NULL;
    demo_data.canned_data = info;
    demo_data.camera_objhandle = camera_handle;
    Cinematic_WriteDemoFileData(&demo_data);
  }

  switch (info->type) {
  case CANNED_LEVEL_INTRO:
    CannedCinematic_Intro(info->camera_pathid, info->text_to_display, info->target_pathid, info->time, camera_handle);
    break;
  case CANNED_LEVEL_END_PATH:
    CannedCinematic_EndLevelPath(info->camera_pathid, info->text_to_display, info->target_pathid, info->time,
                                 camera_handle);
    break;
  case CANNED_LEVEL_END_POINT: {
    object *cam = ObjGet(info->object_to_use_for_point);
    if (cam) {
      CannedCinematic_EndLevelPoint(&cam->pos, cam->roomnum, info->text_to_display, info->target_pathid, info->time,
                                    camera_handle);
    } else {
      mprintf(0, "Passed in object handle for canned cinematic (level end) for camera position does not exist\n");
      Cinematic_DeleteCamera(camera_handle);
      SetGameState(GAMESTATE_LVLEND);
    }
  } break;
  case CANNED_MOVE_PLAYER_FADE: {
    object *player = ObjGet(info->target_objhandle);
    if (!player || player->type != OBJ_PLAYER) {
      mprintf(0, "Invalid player passed to CANNED_MOVE_PLAYER_FADE\n");
      Cinematic_DeleteCamera(camera_handle);
      return;
    }

    CannedCinematic_MovePlayerFade(player, info->room, &info->pos, &info->orient, camera_handle);

  } break;
  case CANNED_LEVEL_END_FADE_WHITE: {
    CannedCinematic_LevelEndFadeWhite(camera_handle, info->time, info->text_to_display);
  } break;
  default: {
    Cinematic_DeleteCamera(camera_handle);
  } break;
  }
}

//==================================================
// Demo file support
//==================================================
static void mf_WriteInt(uint8_t *buffer, int *pointer, int data);
static void mf_WriteShort(uint8_t *buffer, int *pointer, int16_t data);
static void mf_WriteByte(uint8_t *buffer, int *pointer, uint8_t data);
static void mf_WriteFloat(uint8_t *buffer, int *pointer, float data);
static void mf_WriteBytes(uint8_t *buffer, int *pointer, uint8_t *data, int len);
static void mf_WriteString(uint8_t *buffer, int *pointer, const char *string);
static int mf_ReadInt(uint8_t *buffer, int *pointer);
static int16_t mf_ReadShort(uint8_t *buffer, int *pointer);
static uint8_t mf_ReadByte(uint8_t *buffer, int *pointer);
static float mf_ReadFloat(uint8_t *buffer, int *pointer);
static void mf_ReadBytes(uint8_t *buffer, int *pointer, uint8_t *data, int len);
static void mf_ReadString(uint8_t *buffer, int *pointer, char *string);

void Cinematic_DoDemoFileData(uint8_t *buffer) {
  int count = 0;
  char text_string[512];
  tCannedCinematicInfo canned_data;
  tGameCinematic cinematic_data;
  int old_camera_objnum, camera_objnum, camera_handle;

  // see what kind of cinematic we are loading
  switch (mf_ReadByte(buffer, &count)) {
  case CDI_NOT_CANNED: {
    matrix fake_orient;
    vm_MakeIdentity(&fake_orient);

    cinematic_data.orient = &fake_orient;
    cinematic_data.callback = NULL;
    cinematic_data.flags = mf_ReadInt(buffer, &count);
    cinematic_data.target_objhandle = mf_ReadInt(buffer, &count);
    cinematic_data.end_transition = mf_ReadInt(buffer, &count);
    cinematic_data.start_transition = mf_ReadInt(buffer, &count);
    cinematic_data.pathid = mf_ReadInt(buffer, &count);
    cinematic_data.position.x = mf_ReadFloat(buffer, &count);
    cinematic_data.position.y = mf_ReadFloat(buffer, &count);
    cinematic_data.position.z = mf_ReadFloat(buffer, &count);
    cinematic_data.room = mf_ReadInt(buffer, &count);
    if (mf_ReadByte(buffer, &count)) {
      fake_orient.fvec.x = mf_ReadFloat(buffer, &count);
      fake_orient.fvec.y = mf_ReadFloat(buffer, &count);
      fake_orient.fvec.z = mf_ReadFloat(buffer, &count);
      fake_orient.uvec.x = mf_ReadFloat(buffer, &count);
      fake_orient.uvec.y = mf_ReadFloat(buffer, &count);
      fake_orient.uvec.z = mf_ReadFloat(buffer, &count);
      fake_orient.rvec.x = mf_ReadFloat(buffer, &count);
      fake_orient.rvec.y = mf_ReadFloat(buffer, &count);
      fake_orient.rvec.z = mf_ReadFloat(buffer, &count);
    }

    cinematic_data.max_time_play = mf_ReadFloat(buffer, &count);
    cinematic_data.text_display.max = mf_ReadFloat(buffer, &count);
    cinematic_data.text_display.min = mf_ReadFloat(buffer, &count);
    cinematic_data.track_target.max = mf_ReadFloat(buffer, &count);
    cinematic_data.track_target.min = mf_ReadFloat(buffer, &count);
    cinematic_data.player_disabled.max = mf_ReadFloat(buffer, &count);
    cinematic_data.player_disabled.min = mf_ReadFloat(buffer, &count);
    cinematic_data.in_camera_view.max = mf_ReadFloat(buffer, &count);
    cinematic_data.in_camera_view.min = mf_ReadFloat(buffer, &count);
    cinematic_data.quick_exit.max = mf_ReadFloat(buffer, &count);
    cinematic_data.quick_exit.min = mf_ReadFloat(buffer, &count);
    mf_ReadString(buffer, &count, text_string);

    old_camera_objnum = mf_ReadInt(buffer, &count);
    ASSERT(old_camera_objnum >= 0 && old_camera_objnum < MAX_OBJECTS);
    camera_objnum = Demo_obj_map[old_camera_objnum];
    ASSERT(camera_objnum >= 0 && camera_objnum < MAX_OBJECTS && Objects[camera_objnum].type == OBJ_CAMERA);
    camera_handle = Objects[camera_objnum].handle;

    Cinematic_StartCine(&cinematic_data, text_string, camera_handle);

  } break;
  case CDI_CANNED: {
    canned_data.type = mf_ReadInt(buffer, &count);
    canned_data.camera_pathid = mf_ReadInt(buffer, &count);
    canned_data.target_pathid = mf_ReadInt(buffer, &count);
    canned_data.target_objhandle = mf_ReadInt(buffer, &count);
    canned_data.time = mf_ReadFloat(buffer, &count);
    canned_data.object_to_use_for_point = mf_ReadInt(buffer, &count);
    canned_data.room = mf_ReadInt(buffer, &count);
    canned_data.pos.x = mf_ReadFloat(buffer, &count);
    canned_data.pos.y = mf_ReadFloat(buffer, &count);
    canned_data.pos.z = mf_ReadFloat(buffer, &count);
    canned_data.orient.fvec.x = mf_ReadFloat(buffer, &count);
    canned_data.orient.fvec.y = mf_ReadFloat(buffer, &count);
    canned_data.orient.fvec.z = mf_ReadFloat(buffer, &count);
    canned_data.orient.uvec.x = mf_ReadFloat(buffer, &count);
    canned_data.orient.uvec.y = mf_ReadFloat(buffer, &count);
    canned_data.orient.uvec.z = mf_ReadFloat(buffer, &count);
    canned_data.orient.rvec.x = mf_ReadFloat(buffer, &count);
    canned_data.orient.rvec.y = mf_ReadFloat(buffer, &count);
    canned_data.orient.rvec.z = mf_ReadFloat(buffer, &count);

    mf_ReadString(buffer, &count, text_string);
    canned_data.text_to_display = text_string;

    old_camera_objnum = mf_ReadInt(buffer, &count);
    ASSERT(old_camera_objnum >= 0 && old_camera_objnum < MAX_OBJECTS);
    camera_objnum = Demo_obj_map[old_camera_objnum];
    ASSERT(camera_objnum >= 0 && camera_objnum < MAX_OBJECTS && Objects[camera_objnum].type == OBJ_CAMERA);
    camera_handle = Objects[camera_objnum].handle;

    Cinematic_StartCanned(&canned_data, camera_handle);

  } break;
  }
}

void Cinematic_WriteDemoFileData(tCinematicDemoInfo *info) {
  uint8_t buffer[1500];
  int count = 0;

  // what kind of cinematic are we saving
  mf_WriteByte(buffer, &count, info->cinematic_type);

  switch (info->cinematic_type) {
  case CDI_NOT_CANNED: {

    mf_WriteInt(buffer, &count, info->cinematic_data->flags);
    mf_WriteInt(buffer, &count, info->cinematic_data->target_objhandle);
    mf_WriteInt(buffer, &count, info->cinematic_data->end_transition);
    mf_WriteInt(buffer, &count, info->cinematic_data->start_transition);
    mf_WriteInt(buffer, &count, info->cinematic_data->pathid);
    mf_WriteFloat(buffer, &count, info->cinematic_data->position.x);
    mf_WriteFloat(buffer, &count, info->cinematic_data->position.y);
    mf_WriteFloat(buffer, &count, info->cinematic_data->position.z);
    mf_WriteInt(buffer, &count, info->cinematic_data->room);

    if (info->cinematic_data->orient) {
      mf_WriteByte(buffer, &count, 1);
      mf_WriteFloat(buffer, &count, info->cinematic_data->orient->fvec.x);
      mf_WriteFloat(buffer, &count, info->cinematic_data->orient->fvec.y);
      mf_WriteFloat(buffer, &count, info->cinematic_data->orient->fvec.z);
      mf_WriteFloat(buffer, &count, info->cinematic_data->orient->uvec.x);
      mf_WriteFloat(buffer, &count, info->cinematic_data->orient->uvec.y);
      mf_WriteFloat(buffer, &count, info->cinematic_data->orient->uvec.z);
      mf_WriteFloat(buffer, &count, info->cinematic_data->orient->rvec.x);
      mf_WriteFloat(buffer, &count, info->cinematic_data->orient->rvec.y);
      mf_WriteFloat(buffer, &count, info->cinematic_data->orient->rvec.z);

    } else {
      mf_WriteByte(buffer, &count, 0);
    }

    mf_WriteFloat(buffer, &count, info->cinematic_data->max_time_play);
    mf_WriteFloat(buffer, &count, info->cinematic_data->text_display.max);
    mf_WriteFloat(buffer, &count, info->cinematic_data->text_display.min);
    mf_WriteFloat(buffer, &count, info->cinematic_data->track_target.max);
    mf_WriteFloat(buffer, &count, info->cinematic_data->track_target.min);
    mf_WriteFloat(buffer, &count, info->cinematic_data->player_disabled.max);
    mf_WriteFloat(buffer, &count, info->cinematic_data->player_disabled.min);
    mf_WriteFloat(buffer, &count, info->cinematic_data->in_camera_view.max);
    mf_WriteFloat(buffer, &count, info->cinematic_data->in_camera_view.min);
    mf_WriteFloat(buffer, &count, info->cinematic_data->quick_exit.max);
    mf_WriteFloat(buffer, &count, info->cinematic_data->quick_exit.min);

    if (info->text_string)
      mf_WriteString(buffer, &count, info->text_string);
    else
      mf_WriteByte(buffer, &count, 0);

    object *obj = ObjGet(info->camera_objhandle);
    ASSERT(obj && obj->type == OBJ_CAMERA);
    mf_WriteInt(buffer, &count, OBJNUM(obj));

  } break;
  case CDI_CANNED: {

    mf_WriteInt(buffer, &count, info->canned_data->type);
    mf_WriteInt(buffer, &count, info->canned_data->camera_pathid);
    mf_WriteInt(buffer, &count, info->canned_data->target_pathid);
    mf_WriteInt(buffer, &count, info->canned_data->target_objhandle);
    mf_WriteFloat(buffer, &count, info->canned_data->time);
    mf_WriteInt(buffer, &count, info->canned_data->object_to_use_for_point);
    mf_WriteInt(buffer, &count, info->canned_data->room);
    mf_WriteFloat(buffer, &count, info->canned_data->pos.x);
    mf_WriteFloat(buffer, &count, info->canned_data->pos.y);
    mf_WriteFloat(buffer, &count, info->canned_data->pos.z);
    mf_WriteFloat(buffer, &count, info->canned_data->orient.fvec.x);
    mf_WriteFloat(buffer, &count, info->canned_data->orient.fvec.y);
    mf_WriteFloat(buffer, &count, info->canned_data->orient.fvec.z);
    mf_WriteFloat(buffer, &count, info->canned_data->orient.uvec.x);
    mf_WriteFloat(buffer, &count, info->canned_data->orient.uvec.y);
    mf_WriteFloat(buffer, &count, info->canned_data->orient.uvec.z);
    mf_WriteFloat(buffer, &count, info->canned_data->orient.rvec.x);
    mf_WriteFloat(buffer, &count, info->canned_data->orient.rvec.y);
    mf_WriteFloat(buffer, &count, info->canned_data->orient.rvec.z);

    if (info->canned_data->text_to_display)
      mf_WriteString(buffer, &count, info->canned_data->text_to_display);
    else
      mf_WriteByte(buffer, &count, 0);

    object *obj = ObjGet(info->camera_objhandle);
    ASSERT(obj && obj->type == OBJ_CAMERA);
    mf_WriteInt(buffer, &count, OBJNUM(obj));

  } break;
  }

  DemoWriteCinematics(buffer, count);
}

void mf_WriteInt(uint8_t *buffer, int *pointer, int data) {
  memcpy(&buffer[(*pointer)], &data, sizeof(int));
  (*pointer) += sizeof(int);
}

void mf_WriteShort(uint8_t *buffer, int *pointer, int16_t data) {
  memcpy(&buffer[(*pointer)], &data, sizeof(int16_t));
  (*pointer) += sizeof(int16_t);
}

void mf_WriteByte(uint8_t *buffer, int *pointer, uint8_t data) {
  memcpy(&buffer[(*pointer)], &data, sizeof(uint8_t));
  (*pointer) += sizeof(uint8_t);
}

void mf_WriteFloat(uint8_t *buffer, int *pointer, float data) {
  memcpy(&buffer[(*pointer)], &data, sizeof(float));
  (*pointer) += sizeof(float);
}

void mf_WriteBytes(uint8_t *buffer, int *pointer, uint8_t *data, int len) {
  memcpy(&buffer[(*pointer)], data, len);
  (*pointer) += len;
}

void mf_WriteString(uint8_t *buffer, int *pointer, const char *string) {
  while (*string) {
    mf_WriteByte(buffer, pointer, (*string));
    string++;
  }
  mf_WriteByte(buffer, pointer, (*string));
}

int mf_ReadInt(uint8_t *buffer, int *pointer) {
  int value;
  memcpy(&value, &buffer[(*pointer)], sizeof(value));
  (*pointer) += sizeof(value);
  return value;
}

int16_t mf_ReadShort(uint8_t *buffer, int *pointer) {
  int16_t value;
  memcpy(&value, &buffer[(*pointer)], sizeof(value));
  (*pointer) += sizeof(value);
  return value;
}

uint8_t mf_ReadByte(uint8_t *buffer, int *pointer) {
  uint8_t value;
  memcpy(&value, &buffer[(*pointer)], sizeof(value));
  (*pointer) += sizeof(value);
  return value;
}

float mf_ReadFloat(uint8_t *buffer, int *pointer) {
  float value;
  memcpy(&value, &buffer[(*pointer)], sizeof(value));
  (*pointer) += sizeof(value);
  return value;
}

void mf_ReadBytes(uint8_t *buffer, int *pointer, uint8_t *data, int len) {
  memcpy(data, &buffer[(*pointer)], len);
  (*pointer) += len;
}

void mf_ReadString(uint8_t *buffer, int *pointer, char *string) {
  uint8_t data;
  data = mf_ReadByte(buffer, pointer);
  while (data) {
    *string = data;
    string++;
    data = mf_ReadByte(buffer, pointer);
  }
  *string = '\0';
}
