/*=============================================================================
    Name    : NIS.c
    Purpose : Code for animating NIS's

    Created 12/3/1997 by lmoloney
    Copyright Relic Entertainment, Inc.  All rights reserved.
=============================================================================*/

#ifndef SW_Render
#include <windows.h>
#endif
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <float.h>
#include "glinc.h"
#include "types.h"
#include "file.h"
#include "debug.h"
#include "memory.h"
#include "task.h"
#include "universe.h"
#include "univupdate.h"
#include "spaceobj.h"
#include "camera.h"
#include "mex.h"
#include "select.h"
#include "b-spline.h"
#include "fastmath.h"
#include "attack.h"
#include "soundevent.h"
#include "light.h"
#include "btg.h"
#include "nis.h"
#include "glcaps.h"
#include "randy.h"
#include "region.h"
#include "feflow.h"
#include "mouse.h"
#include "soundevent.h"
#include "mainrgn.h"
#include "kas.h"
#include "commandWrap.h"
#include "strings.h"
#include "bink.h"
#include "render.h"
#include "eval.h"
#include "tracking.h"

/*=============================================================================
    Data:
=============================================================================*/
extern regionhandle ghMainRegion;
bool nisScreenStarted = FALSE;

bool nisIsRunning = FALSE;
bool nisCaptureCamera = FALSE;
bool nisEnabled = TRUE;
Camera *nisCamera = NULL;

#if NIS_TIME_CONTROLS
real32 nisPlayFactor = 1.0f;
#endif
bool nisPaused = FALSE;

#if NIS_PRINT_INFO
char nisInfoString[256] = "";
bool nisPrintInfo = FALSE;
bool nisNoLockout = FALSE;
#endif

//for scanning the script part of an NIS
nisevent *nisEventList = NULL;
sdword nisEventIndex;

void nisDeathAnything(nisplaying *NIS, nisevent *event);
void nisAttack(nisplaying *NIS, nisevent *event);
void nisHaltAttack(nisplaying *NIS, nisevent *event);
void nisFire(nisplaying *NIS, nisevent *event);
void nisObjectShow(nisplaying *NIS, nisevent *event);
void nisObjectHide(nisplaying *NIS, nisevent *event);
void nisInvincible(nisplaying *NIS, nisevent *event);
void nisVincible(nisplaying *NIS, nisevent *event);
void nisSoundEvent(nisplaying *NIS, nisevent *event);
void nisSpeechEvent(nisplaying *NIS, nisevent *event);
void nisFleetSpeechEvent(nisplaying *NIS, nisevent *event);
void nisAnimaticSpeechEvent(nisplaying *NIS, nisevent *event);
void nisDisableDefaultSpeech(nisplaying *NIS, nisevent *event);
void nisEnableDefaultSpeech(nisplaying *NIS, nisevent *event);
void nisCustomEffect(nisplaying *NIS, nisevent *event);
void nisCustomGunEffect(nisplaying *NIS, nisevent *event);
void nisGunShoot(nisplaying *NIS, nisevent *event);
void nisRemainAtEnd(nisplaying *NIS, nisevent *event);
void nisCameraFOV(nisplaying *NIS, nisevent *event);
void nisCameraCut(nisplaying *NIS, nisevent *event);
void nisTextCard(nisplaying *NIS, nisevent *event);
void nisScissorOut(nisplaying *NIS, nisevent *event);
void nisFocusAtEnd(nisplaying *NIS, nisevent *event);
void nisFocus(nisplaying *NIS, nisevent *event);
void nisMusicStart(nisplaying *NIS, nisevent *event);
void nisMusicStop(nisplaying *NIS, nisevent *event);
void nisBlackFadeSet(nisplaying *NIS, nisevent *event);
void nisBlackFadeTo(nisplaying *NIS, nisevent *event);
void nisColorScheme(nisplaying *NIS, nisevent *event);
void nisMeshAnimationStart(nisplaying *NIS, nisevent *event);
void nisMeshAnimationStop(nisplaying *NIS, nisevent *event);
void nisMeshAnimationPause(nisplaying *NIS, nisevent *event);
void nisMeshAnimationSeek(nisplaying *NIS, nisevent *event);
void nisBlendCameraEndPoint(nisplaying *NIS, nisevent *event);
void nisTextScroll(nisplaying *NIS, nisevent *event);
void nisVolumeSet(nisplaying *NIS, nisevent *event);
void nisTrailMove(nisplaying *NIS, nisevent *event);
void nisTrailZero(nisplaying *NIS, nisevent *event);
void nisUniversePauseToggle(nisplaying *NIS, nisevent *event);
void nisUniverseHideToggle(nisplaying *NIS, nisevent *event);
void nisNewEnvironment(nisplaying *NIS, nisevent *event);
void nisSMPTEOn(nisplaying *NIS, nisevent *event);
void nisSMPTEOff(nisplaying *NIS, nisevent *event);
void nisStaticOn(nisplaying *NIS, nisevent *event);
void nisStaticOff(nisplaying *NIS, nisevent *event);
void nisHideDerelict(nisplaying *NIS, nisevent *event);
void nisNewNIS(nisplaying *NIS, nisevent *event);
void nisAttributes(nisplaying *NIS, nisevent *event);
void nisAttackKasSelection(nisplaying *NIS, nisevent *event);
void nisKeepMovingAtEnd(nisplaying *NIS, nisevent *event);
void nisMinimumLOD(nisplaying *NIS, nisevent *event);
void nisDamageLevel(nisplaying *NIS, nisevent *event);
niseventdispatch nisEventDispatch[] =
{
    /*NEO_DeathDamage,          */ nisDeathAnything,
    /*NEO_DeathProjectile,      */ nisDeathAnything,
    /*NEO_DeathBeam,            */ nisDeathAnything,
    /*NEO_DeathCollision,       */ NULL,
    /*NEO_Attack,               */ nisAttack,
    /*NEO_HaltAttack,           */ nisHaltAttack,
    /*NEO_Fire,                 */ nisFire,
    /*NEO_Show                  */ nisObjectShow,
    /*NEO_Hide                  */ nisObjectHide,
    /*NEO_Invincible,           */ nisInvincible,
    /*NEO_Vincible,             */ nisVincible,
    /*NEO_SoundEvent,           */ nisSoundEvent,
    /*NEO_SpeechEvent,          */ nisSpeechEvent,
    /*NEO_DisableDefaultSpeech  */ NULL,
    /*NEO_EnableDefaultSpeech,  */ NULL,
    /*NEO_CustomEffect,         */ NULL,
    /*NEO_CustomGunEffect,      */ NULL,
    /*NEO_GunShoot,             */ NULL,
    /*NEO_RemainAtEnd,          */ nisRemainAtEnd,
    /*NEO_FleetSpeechEvent,     */ nisFleetSpeechEvent,
    /*NEO_CameraFOV,            */ nisCameraFOV,
    /*NEO_CameraCut,            */ nisCameraCut,
    /*NEO_BlackScreenStart,     */ NULL,
    /*NEO_BlackScreenEnd,       */ NULL,
    /*NEO_TextCard,             */ nisTextCard,
    /*NEO_TextScroll,           */ nisTextScroll,
    /*NEO_ScissorOut,           */ nisScissorOut,
    /*NEO_Focus,                */ nisFocus,
    /*NEO_FocusAtEnd            */ nisFocusAtEnd,
    /*NEO_StartMusic            */ nisMusicStart,
    /*NEO_StopMusic             */ nisMusicStop,
    /*NEO_BlackFadeSet          */ nisBlackFadeSet,
    /*NEO_BlackFadeTo           */ nisBlackFadeTo,
    /*NEO_ColorScheme           */ nisColorScheme,
    /*NEO_MeshAnimationStart,   */ nisMeshAnimationStart,
    /*NEO_MeshAnimationStop,    */ nisMeshAnimationStop,
    /*NEO_MeshAnimationPause,   */ nisMeshAnimationPause,
    /*NEO_MeshAnimationSeek,    */ nisMeshAnimationSeek,
    /*NEO_BlendCameraEndPoint   */ nisBlendCameraEndPoint,
    /*NEO_MusicVolume,          */ nisVolumeSet,
    /*NEO_SpeechVolume,         */ nisVolumeSet,
    /*NEO_SFXVolume,            */ nisVolumeSet,
    /*NEO_TrailMove,            */ nisTrailMove,
    /*NEO_TrailZeroLength,      */ nisTrailZero,
    /*NEO_UniversePauseToggle   */ nisUniversePauseToggle,
    /*NEO_UniverseHideToggle    */ nisUniverseHideToggle,
    /*NEO_NewEnvironment,       */ nisNewEnvironment,
    /*NEO_LinkEnvironment,      */ NULL,
    /*NEO_AnimaticSpeechEvent,  */ nisAnimaticSpeechEvent,
    /*NEO_SMPTEOn,              */ nisSMPTEOn,
    /*NEO_SMPTEOff,             */ nisSMPTEOff,
    /*NEO_StaticOn,             */ nisStaticOn,
    /*NEO_StaticOff,            */ nisStaticOff,
    /*NEO_HideDerelict          */ nisHideDerelict,
    /*NEO_ShowDerelict          */ nisHideDerelict,
    /*NEO_StartNewNIS           */ nisNewNIS,
    /*NEO_Attributes            */ nisAttributes,
    /*NEO_AttackKasSelection    */ nisAttackKasSelection,
    /*NEO_KeepMovingAtEnd       */ nisKeepMovingAtEnd,
    /*NEO_DamageLevel           */ nisDamageLevel,
    /*NEO_MinimumLOD,           */ nisMinimumLOD,
    /*NEO_LastNEO               */ NULL,
};

//default orientation matrix for an NIS ship
matrix HPBIdentityShip = {0.0f, 0.0f, 1.0f, -1.0f, 0.0f, 0.0f, 0.0f, -1.0f, 0.0f};

//global variables for parsing script files
sdword nisCurrentObject = -1;                   //current object in the script file for subsequent events
nisheader *nisCurrentHeader = NULL;              //current NIS header to go with our current script file
real32 nisCurrentTime = REALlyBig;              //current time for subsequent events

nisheader *testHeader;
nisplaying *testPlaying = 0;
taskhandle nisTaskHandle;
bool nisTaskToPause = FALSE;                    //task to be paused at end of loading
bool nisTaskToResume = FALSE;                   //task to be resumed at end of loading

nisplaying *thisNisPlaying = NULL;
nisheader *thisNisHeader = NULL;
nisDoneCB thisNisDoneCB = NULL;

//information for NIS text cards
nistextcard nisTextCardList[NIS_NumberTextCards];
sdword nisTextCardIndex = 0;
color nisCurrentTextCardColor = colWhite;
sdword nisCurrentTextCardX = 0, nisCurrentTextCardY = 0;
sdword nisCurrentTextMargin = 0;
fonthandle nisCurrentTextCardFont = 0;
real32 nisCurrentDuration = 0.0f;
real32 nisCurrentTextFadeIn = 0.0f;
real32 nisCurrentTextFadeOut = 0.0f;
real32 nisTextScrollStart = 0.0f;
real32 nisTextScrollEnd = 0.0f;
real32 nisTextScrollDistance = 0.0f;

//information for zooming the camera and fading in the scissor window
real32 nisCameraCutTime = 0.0f;
real32 nisCameraCutStage2 = -1.0f;
bool nisCameraCutOut = FALSE;
tcb nisCameraParams[NIS_NCameraKeys] =
{                                               //b-spline parameters
    {0.8f, 0.0f, 0.0f},
    {0.8f, 0.0f, 0.0f},
    {9.9f, 9.0f, 9.9f},                         //dummy middle keyframe
    {0.8f, 0.0f, 0.0f},
    {0.8f, 0.0f, 0.0f},
};
tcb nisMiddleParam = {1.0f, 0.0f, -0.9f};
real32 nisCameraTimes[NIS_NCameraKeys] = {0.0f, 0.0f, 0.0f, 0.0f};//b-spline times
real32 nisCameraPoints[9][NIS_NCameraKeys];     //b-spline data points
splinecurve *nisCameraCurve[9];                 //actual b-splines
real32 nisScissorFadeIn = 0.0f;                 //duration of fade in/out
real32 nisScissorFadeOut = 0.0f;
real32 nisScissorFadeTime = 0.0f;               //current fade time
real32 nisScissorFade = 0.0f;                   //current fade amount (0..1)
real32 nisBlackFade = 0.0f;                     //the current fade level (0 = normal, 1 = fully faded out)
real32 nisBlackFadeDest = 0.0f;                 //what we're fading to
real32 nisBlackFadeRate = 0.0f;                 //at what speed
bool nisFullyScissored = FALSE;                 //is scissor fully opaque?
bool bCameraFocussed;                           //set to TRUE if the camera was focussed on command of the script file
bool bPerfectFocusComputed;                     //set to TRUE when nisBlendCameraEndPoint called
bool nisSeeking = FALSE;                        //set to TRUE during seeks
bool nisNewNISStarted = FALSE;                  //set to TRUE when a new NIS is started by an event
real32 nisPreviousCameraDistance = 10.0f;       //zoom distance of the camera when the NIS started

//music playing stuffs.
real32 nisPreviousSFXVolume, nisPreviousSpeechVolume, nisPreviousMusicVolume;
bool nisMusicPlaying = FALSE;
//for pausing/hiding the universe
bool8 nisUniversePause = FALSE;
bool8 nisUniverseHidden = FALSE;
real32 nisUnivStartTime;

//for changing environment zones
char *nisPreviousLighting;
char *nisPreviousBackground;
real32 nisPreviousTheta;
real32 nisPreviousPhi;

//for the SMPTE counter
nisSMPTE *nisSMPTECounter = NULL;

//for on-screen static
nisstatic *nisStatic[NIS_NumberStatics + NIS_NumberExtraStatics] = {NULL, NULL, NULL, NULL};

//for localization
char *nisLanguageSubpath[] =
{
    "",
    "French\\",
    "German\\",
    "",         // Spanish uses the same script settings as English as the audio isn't localized
    ""          // Italian uses the same script settings as English as the audio isn't localized
};

/*=============================================================================
    Functions:
=============================================================================*/

#if NIS_TEST
char *nisTestNIS = NULL;
char *nisTestScript = NULL;
vector nisStartingPosition;
matrix nisStartingMatrix;
sdword nisTestIndex = 0;
void nisNamesFind(sdword index, char **NIS, char **script)
{
    filehandle fp;
    char *parse;

    parse = memAlloc(_MAX_PATH * 2, "Sdfgadsf", 0);
    *NIS = *script = NULL;
    if (!fileExists(nisTestNIS, 0))
    {
        return;
    }
    fp = fileOpen(nisTestNIS, FF_TextMode);
    for (; index >= 0; index--)
    {
        if (fileLineRead(fp, parse, _MAX_PATH * 2) == FR_EndOfFile)
        {
            fileClose(fp);
            return;
        }
    }
    *NIS = parse;
    strtok(parse, " \n\t,");
    *script = strtok(NULL, " \n\t,");
    fileClose(fp);
    return;
}
void nisTest(vector *position, matrix *mat)
{
    char *nisFile, *nisScript;
    if (nisTestNIS == NULL)
    {
        return;
    }
    nisStartingPosition = *position;
    nisStartingMatrix = *mat;

    nisNamesFind(nisTestIndex, &nisFile, &nisScript);
    if (nisFile == NULL)
    {
        dbgMessagef("\nCan't find %d's NIS is file '%s'", nisTestIndex, nisFile);
        return;
    }
    dbgMessagef("\nnisTest: playing NIS '%s' with script '%s'.", nisFile, nisScript);
    testHeader = nisLoad(nisFile, nisScript);
    testHeader->loop = 0.0f;            //force the NIS to loop
    testPlaying = nisStart(testHeader, &nisStartingPosition, &nisStartingMatrix);
    memFree(nisFile);
    taskResume(nisTaskHandle);
}
void nisTestAnother(sdword skip)
{
    char *nisFile, *nisScript;
    nisStop(testPlaying);
    nisDelete(testHeader);
    nisTestIndex += skip;
    nisTestIndex = max(nisTestIndex, 0);
    nisNamesFind(nisTestIndex, &nisFile, &nisScript);
    if (nisFile == NULL)
    {
        dbgMessagef("\nCan't find %d's NIS is file '%s'", nisTestIndex, nisFile);
        return;
    }
    dbgMessagef("\nnisTest: playing NIS '%s' with script '%s'.", nisFile, nisScript);
    testHeader = nisLoad(nisFile, nisScript);
    testHeader->loop = 0.0f;            //force the NIS to loop
    testPlaying = nisStart(testHeader, &nisStartingPosition, &nisStartingMatrix);
    memFree(nisFile);
}
#endif

/*-----------------------------------------------------------------------------
    Name        : nisCameraFlyCompute
    Description : Compute the camera location for flying it into the starting position
    Inputs      : timeElapsed - time step for flying in
    Outputs     :
    Return      :
----------------------------------------------------------------------------*/
void nisCameraFlyCompute(real32 timeElapsed)
{
    vector eye, look, up;

    eye.x = bsCurveUpdate(nisCameraCurve[0], timeElapsed);
    if (eye.x == REALlyBig)
    {                                           //if any curve has expired
        //...end the camera cut, start the NIS
        nisPause(FALSE);
        nisCameraCutTime = 0.0f;                //done with the camera cuts
        nisCameraCutStage2 = -1.0f;
    }
    else
    {
        eye.y = bsCurveUpdate(nisCameraCurve[1], timeElapsed);
        eye.z = bsCurveUpdate(nisCameraCurve[2], timeElapsed);
        look.x = bsCurveUpdate(nisCameraCurve[3], timeElapsed);
        look.y = bsCurveUpdate(nisCameraCurve[4], timeElapsed);
        look.z = bsCurveUpdate(nisCameraCurve[5], timeElapsed);
        up.x = bsCurveUpdate(nisCameraCurve[6], timeElapsed);
        up.y = bsCurveUpdate(nisCameraCurve[7], timeElapsed);
        up.z = bsCurveUpdate(nisCameraCurve[8], timeElapsed);
        vecNormalize(&up);
        dbgAssert(eye.x != REALlyBig && eye.y != REALlyBig && eye.z != REALlyBig);
        dbgAssert(look.x != REALlyBig && look.y != REALlyBig && look.z != REALlyBig);
        dbgAssert(up.x != REALlyBig && up.y != REALlyBig && up.z != REALlyBig);
        nisCamera->eyeposition = eye;
        nisCamera->lookatpoint = look;
        nisCamera->upvector = up;
    }
}

/*-----------------------------------------------------------------------------
    Name        : nisUpdateTask
    Description : Task function for updating NIS's
    Inputs      :
    Outputs     :
    Return      :
----------------------------------------------------------------------------*/
//void utyTeaserEnd(void);
extern nisplaying *utyTeaserPlaying;
#pragma optimize("gy", off)                       //turn on stack frame (we need ebp for this function)
void nisUpdateTask(void)
{
    static real32 newTime;
    static real32 timeElapsed;

    taskYield(0);

    while (1)
    {
        taskStackSaveCond(0);
        //code for playing in-game NIS's
        if (nisScissorFadeOut != 0)
        {                                                   //if fading the scissor window out
            nisScissorFadeTime += (real32)UNIVERSE_UPDATE_PERIOD;
            nisFullyScissored = FALSE;                      //window no longer fully scissored
            if (nisScissorFadeTime >= nisScissorFadeOut)
            {                                               //if at the end of the fade
                nisScissorFade = nisScissorFadeOut = nisScissorFadeTime = 0.0f;//don't do any more fading
                if (thisNisPlaying == NULL)
                {
                    taskPause(nisTaskHandle);                   //NIS task all done
                }
            }
            else
            {
                nisScissorFade = 1.0f - (nisScissorFadeTime / nisScissorFadeOut);//set the fade amount
            }
        }

        //fade to/from black
        if (nisBlackFade != nisBlackFadeDest)
        {
            if (abs(nisBlackFade - nisBlackFadeDest) <= UNIVERSE_UPDATE_PERIOD)
            {                                           //if the end of the fade
                nisBlackFade = nisBlackFadeDest;
                nisBlackFadeRate = 0.0f;
            }
            else
            {
                nisBlackFade += nisBlackFadeRate * UNIVERSE_UPDATE_PERIOD;
            }

#if NIS_VERBOSE_LEVEL >= 1
            dbgMessagef("\nfade level = %.2f", nisBlackFade);
#endif
        }

        //fade the scissor window in
        if (nisScissorFadeIn > 0.0f)
        {                                               //if fading window IN
//          soundEvent(NULL, UI_Letterbox);
            nisScissorFade = nisScissorFadeTime / nisScissorFadeIn;//set the fade amount
            if (nisScissorFadeTime >= nisScissorFadeIn)
            {                                           //if at the end of the fade
                nisFullyScissored = TRUE;               //window is fully scissored
                nisScissorFadeIn = nisScissorFadeTime = 0.0f;//don't do any more fading
                nisScissorFade = 0.0f;
            }
            nisScissorFadeTime += UNIVERSE_UPDATE_PERIOD;
        }

        if (thisNisPlaying)
        {
            if (nisCameraCutTime != 0.0f)
            {
                nisPause(TRUE);                             //make sure NIS is paused
            }
            //if (!nisPaused)
            {                                               //actually update NIS if NIS unpaused
                newTime = nisUpdate(thisNisPlaying, 1.0f / (real32)UNIVERSE_UPDATE_RATE);
                if (newTime == REALlyBig)
                {
                    nisStop(thisNisPlaying);
                    nisDelete(thisNisHeader);
                    if (nisScissorFadeOut == 0.0f)
                    {
                        taskPause(nisTaskHandle);
                    }
                    thisNisPlaying = NULL;
                    if (thisNisDoneCB != NULL)
                    {
                        thisNisDoneCB();
                        thisNisDoneCB = NULL;
                    }
                    nisFullyScissored = FALSE;
                }
            }

            //fly the camera in
            if (nisCameraCutTime != 0.0f)
            {
                timeElapsed = UNIVERSE_UPDATE_PERIOD;  //!!! cap camera velocity?
                nisCameraFlyCompute(timeElapsed);
            }

//            taskStackRestoreCond();
        }
        //code for playing test NIS's (similar to above but with special keyboard logic)
#if NIS_TEST
        if (testPlaying)
        {
//            taskStackSaveCond(0);
            newTime = nisUpdate(testPlaying, 1.0f / (real32)UNIVERSE_UPDATE_RATE);
            if (newTime == REALlyBig || keyIsStuck(NUMPAD1))
            {
                keyClearSticky(NUMPAD1);
                nisStop(testPlaying);
                nisDelete(testHeader);
                taskPause(nisTaskHandle);
                testPlaying = NULL;
                nisFullyScissored = TRUE;
            }
//            taskStackRestoreCond();
        }
#endif
        //special-case teaser playing code (must do univupdates and whatnot)
/*
        if (utyTeaserPlaying != NULL)
        {
//            taskStackSaveCond(0);
            if (!gameIsRunning)
            {
                univUpdate(1.0f / (real32)UNIVERSE_UPDATE_RATE);
                soundEventUpdate();
            }
            newTime = nisUpdate(utyTeaserPlaying, 1.0f / (real32)UNIVERSE_UPDATE_RATE);

            if (newTime == REALlyBig)
            {
                if (nisScreenStarted == FALSE)
                {
                    feScreenStart(ghMainRegion, "Main_game_screen");
                    mouseCursorShow();
                }
                utyTeaserEnd();
            }
            if (keyAnyKeyHit() && nisScreenStarted == FALSE)
            {
                keyClearAll();
                feScreenStart(ghMainRegion, "Main_game_screen");
                mouseCursorShow();
                nisScreenStarted = TRUE;
            }
        }
*/
        taskStackRestoreCond();
        taskYield(0);
    }
}
#pragma optimize("", on)

/*-----------------------------------------------------------------------------
    Name        : nisStartup
    Description : Start the NIS module
    Inputs      :
    Outputs     :
    Return      :
----------------------------------------------------------------------------*/
void nisStartup(void)
{
    nisTaskHandle = taskStart(nisUpdateTask, UNIVERSE_UPDATE_PERIOD, 0);
    taskPause(nisTaskHandle);
}

/*-----------------------------------------------------------------------------
    Name        : nisShutdown
    Description : Shut down the NIS module
    Inputs      :
    Outputs     :
    Return      :
----------------------------------------------------------------------------*/
void nisShutdown(void)
{

}

/*-----------------------------------------------------------------------------
    Name        : nisTaskPause
    Description : Set the NIS task to pause at the end of loading.
    Inputs      :
    Outputs     :
    Return      :
----------------------------------------------------------------------------*/
void nisTaskPause(void)
{
    nisTaskToPause = TRUE;
}

/*-----------------------------------------------------------------------------
    Name        : nisTaskResume
    Description : Set the NIS task to resume at the end of loading.
    Inputs      :
    Outputs     :
    Return      :
----------------------------------------------------------------------------*/
void nisTaskResume(void)
{
    nisTaskToResume = TRUE;
}

/*-----------------------------------------------------------------------------
    Name        : nisTaskPauseOrResume
    Description : Executes a queued-up pause or resume at the end of loading.
                  This must be called from a task that will be paused by loading.
    Inputs      :
    Outputs     :
    Return      :
----------------------------------------------------------------------------*/
void nisTaskPauseOrResume(void)
{
    if (nisTaskToPause)
    {
        taskPause(nisTaskHandle);
        nisTaskToPause = FALSE;
    }
    if (nisTaskToResume)
    {
        taskResume(nisTaskHandle);
        nisTaskToResume = FALSE;
    }
}

/*-----------------------------------------------------------------------------
    Name        : nisRotateAboutVector
    Description : Make a rotation matrix about an arbitrary vector
    Inputs      : axis - vector to rotate about (must be a unit vector)
                  radians - angle (in radians) to rotate by
    Outputs     : m - matrix (3x3) to fill
    Return      :
----------------------------------------------------------------------------*/
void nisRotateAboutVector(real32* m, vector* axis, real32 radians)
{
    real32 s, c;
    real32 xx, yy, zz, xy, yz, zx, xs, ys, zs, one_c;
    double rads;
    real32 x = axis->x;
    real32 y = axis->y;
    real32 z = axis->z;

    rads = (double)radians;
    s = (real32)sin(rads);
    c = (real32)cos(rads);

#define M(row,col) m[((col<<1) + col)+row]

    xx = x*x;
    yy = y*y;
    zz = z*z;
    xy = x*y;
    yz = y*z;
    zx = z*x;
    xs = x*s;
    ys = y*s;
    zs = z*s;
    one_c = 1.0f - c;

    M(0,0) = (one_c * xx) + c;
    M(0,1) = (one_c * xy) - zs;
    M(0,2) = (one_c * zx) + ys;

    M(1,0) = (one_c * xy) + zs;
    M(1,1) = (one_c * yy) + c;
    M(1,2) = (one_c * yz) - xs;

    M(2,0) = (one_c * zx) - ys;
    M(2,1) = (one_c * yz) + xs;
    M(2,2) = (one_c * zz) + c;

#undef M
}
/*-----------------------------------------------------------------------------
    Name        : nisShipEulerToMatrix
    Description : Builds a coordinate system matrix from a heading,pitch,bank
                    triplicate.
    Inputs      : rotVector - vectror whose members represent the heading, pitch and bank
    Outputs     : coordsys - where to store the matrix
    Return      :
----------------------------------------------------------------------------*/
void nisShipEulerToMatrix(matrix *coordsys, vector *rotVector)
{
    matrix rotation, tempMatrix;
    vector rotateAbout = {0.0f, 0.0f, 1.0f};

    *coordsys = HPBIdentityShip;
    //rotate about up vector (heading)
    nisRotateAboutVector((real32 *)&rotation, &rotateAbout, -rotVector->x); //negative heading
    matMultiplyMatByMat(&tempMatrix, &rotation, coordsys);
    *coordsys = tempMatrix;
    //rotate about heading vector (pitch in lightwave, bank actually)
    matGetVectFromMatrixCol3(rotateAbout, *coordsys);
    nisRotateAboutVector((real32 *)&rotation, &rotateAbout, rotVector->y); //positive pitch
    matMultiplyMatByMat(&tempMatrix, &rotation, coordsys);
    *coordsys = tempMatrix;
    //rotate about heading vector (bank in lightwave, pitch actually)
    matGetVectFromMatrixCol2(rotateAbout, *coordsys);
    nisRotateAboutVector((real32 *)&rotation, &rotateAbout, -rotVector->z); //positive bank
    matMultiplyMatByMat(&tempMatrix, &rotation, coordsys);
    *coordsys = tempMatrix;
}

/*-----------------------------------------------------------------------------
    Name        : nisObjectEulerToMatrix
    Description : Builds a coordinate system matrix from a heading,pitch,bank
                    triplicate.
    Inputs      : rotVector - vectror whose members represent the heading, pitch and bank
    Outputs     : coordsys - where to store the matrix
    Return      :
----------------------------------------------------------------------------*/
void nisObjectEulerToMatrix(matrix *coordsys, vector *rotVector)
{
    matrix rotation, tempMatrix;
    vector rotateAbout = {1.0f, 0.0f, 0.0f};

    *coordsys = IdentityMatrix;
    //rotate about up vector (heading)
    nisRotateAboutVector((real32 *)&rotation, &rotateAbout, -rotVector->x); //negative heading
    matMultiplyMatByMat(&tempMatrix, &rotation, coordsys);
    *coordsys = tempMatrix;
    //rotate about heading vector (pitch in lightwave, bank actually)
    matGetVectFromMatrixCol3(rotateAbout, *coordsys);
    nisRotateAboutVector((real32 *)&rotation, &rotateAbout, rotVector->y); //positive pitch
    matMultiplyMatByMat(&tempMatrix, &rotation, coordsys);
    *coordsys = tempMatrix;
    //rotate about right vector (bank in lightwave, pitch actually)
    matGetVectFromMatrixCol2(rotateAbout, *coordsys);
    nisRotateAboutVector((real32 *)&rotation, &rotateAbout, -rotVector->z); //positive bank
    matMultiplyMatByMat(&tempMatrix, &rotation, coordsys);
    *coordsys = tempMatrix;
}

/*-----------------------------------------------------------------------------
    Name        : nisNewObjectCreate
    Description : Creates a new nis ship and sets it's position
    Inputs      : index - index of ship we're creating
                  header - header we're creating it from
                  path - path for this ship
    Outputs     :
    Return      : pointer to new ship
----------------------------------------------------------------------------*/
Ship *nisNewObjectCreate(nisplaying *NIS, sdword index, nisheader *header, spaceobjpath *path, nisplaying *newHeader, vector *position)
{
    vector startVector, xyzTemp, xyz;
    matrix tempCoordsys;
    Ship *newShip;
    ShipRace race;

    startVector.x = path->curve[0][0];
    startVector.y = path->curve[1][0];
    startVector.z = path->curve[2][0];
    vecAdd(startVector, startVector, *position);
    switch (header->objectPath[index].race)
    {
        case R1:                                            //any ship type
        case R2:
        case P1:
        case P2:
        case P3:
        case Traders:
            race = path->race;
            /*
            if (!(bitTest(GetShipStaticInfo(path->type,path->race)->staticheader.infoFlags, IF_InfoLoaded)))
            {
                if (path->race == R1)       // try other race
                    race = R2;
                else if (path->race == R2)
                    race = R1;
            }
            */
            newShip = univAddShip(path->type, race,                                                //2 is flag to indicate no animation doctoring should occur
                 &startVector, &universe.players[((path->race == universe.players[0].race)) ? 0 : 1],2);//universe.curPlayerPtr);
            newShip->health = REALlyBig;                   //by default, ships are invincible
            univAddObjToRenderList((SpaceObj *)newShip);
            newShip->flags |= SOF_DontApplyPhysics | SOF_Selectable | SOF_Targetable;
            break;
        case NSR_Asteroid:
            newShip = (Ship *)univAddAsteroid(path->type, &startVector);
            newShip->health = REALlyBig;
            break;
        case NSR_DustCloud:
            newShip = (Ship *)univAddDustCloud(path->type, &startVector);
            newShip->health = REALlyBig;
            break;
        case NSR_GasCloud:
            newShip = (Ship *)univAddGasCloud(path->type, &startVector);
            newShip->health = REALlyBig;
            break;
        case NSR_Nebula:
#if NIS_ERROR_CHECKING
            dbgFatal(DBG_Loc, "Can't animate nebulae");
#endif
            break;
        case NSR_Derelict:
            newShip = (Ship *)univAddDerelict(path->type, &startVector);
            newShip->health = REALlyBig;
            break;
        case NSR_Effect:
            newShip = (Ship *)memAlloc(sizeof(SpaceObjRot), "NISEffectSpaceObj", NonVolatile);
            memset(newShip, 0, sizeof(SpaceObjRot));
            newShip->objtype = -1;
            break;
        case NSR_Generic:
            newShip = (Ship *)univAddDerelictByStatInfo(NUM_DERELICTTYPES, header->genericObject[path->type]->staticInfo, &startVector);
            newShip->health = REALlyBig;
            break;
        case NSR_Missile:
            newShip = (Ship *)univAddMissile(path->type);   //type is really the race
            ((Missile *)newShip)->totallifetime = header->length;//missile should last the length of the NIS
            newShip->posinfo.position = startVector;
            break;
        default:;                                            //!!! add asteroids and other stuff
#if NIS_ERROR_CHECKING
            dbgFatalf(DBG_Loc, "Undefined ship race for NIS spaceobj %d: %d.", index, path->race);
#endif
    }
    newShip->flags |= SOF_NISShip;
    if (path->nSamples == NIS_OneKeyframe)
    {                                                       //if only one keyframe in motion path
        //set the position and rotation of the object only once
        //object position:-Z, X, Y (UZI position mode 33)
        xyzTemp.x = -path->curve[2][NIS_FirstKeyFrame];
        xyzTemp.y = path->curve[0][NIS_FirstKeyFrame];
        xyzTemp.z = path->curve[1][NIS_FirstKeyFrame];
        matMultiplyMatByVec(&xyz, &NIS->nisMatrix, &xyzTemp);//multiply NIS world matrix
        newShip->posinfo.position.x = position->x + xyz.x;
        newShip->posinfo.position.y = position->y + xyz.y;
        newShip->posinfo.position.z = position->z + xyz.z;

        startVector.x = path->curve[3][NIS_FirstKeyFrame];
        startVector.y = path->curve[4][NIS_FirstKeyFrame];
        startVector.z = path->curve[5][NIS_FirstKeyFrame];
        nisShipEulerToMatrix(&tempCoordsys, &startVector);
        matMultiplyMatByMat(&newShip->rotinfo.coordsys, &NIS->nisMatrix, &tempCoordsys);

        univUpdateObjRotInfo((SpaceObjRot *)newShip);

        newHeader->objectsInMotion[index].curve[0] = NULL;//no motion path needed
    }
    return(newShip);
}

/*-----------------------------------------------------------------------------
    Name        : nisScissorBarsReset
    Description : Resets the scissor bars and the black fade.
    Inputs      : void
    Outputs     :
    Return      : void
----------------------------------------------------------------------------*/
void nisScissorBarsReset(void)
{
    nisBlackFade = nisBlackFadeDest = 0.0f;                 //no fade is the default
    nisFullyScissored = FALSE;
    nisScissorFadeIn = nisScissorFadeOut = nisScissorFadeTime = nisScissorFade = 0.0f;
}

/*-----------------------------------------------------------------------------
    Name        : nisCameraPrepareForCopy
    Description : Prepares the specified NIS camera to be copied to a real camera.
    Inputs      : cam - camera to monkey with.
    Outputs     :
    Return      :
----------------------------------------------------------------------------*/
void nisCameraPrepareForCopy(Camera *cam)
{
    vector distVect;
#if NIS_ERROR_CHECKING
    real32 magnitudeSq;

    vecSub(distVect, cam->lookatpoint, cam->eyeposition);   //make sure this camera has not already been prepared
    magnitudeSq = vecMagnitudeSquared(distVect);
    dbgAssert(magnitudeSq > NIS_LookScalar * NIS_LookScalar * 0.9f);
    dbgAssert(magnitudeSq < NIS_LookScalar * NIS_LookScalar * 1.1f);
#endif
    vecSub(distVect, cam->lookatpoint, cam->eyeposition);   //eye->look vector
    vecMultiplyByScalar(distVect, 1.0f / NIS_LookScalar);   //scale the vector back to a length of 1.0
    vecAdd(cam->lookatpoint, cam->eyeposition, distVect);   //add back to eye vector
}

/*-----------------------------------------------------------------------------
    Name        : nisStart
    Description : Start an NIS playing
    Inputs      : header - data which contains NIS information
                  position - NIS reference point
                  coordSystem - what rotation to play the NIS at
    Outputs     :
    Return      :
----------------------------------------------------------------------------*/
nisplaying *nisStart(nisheader *header, vector *position, matrix *coordSystem)
{
    nisplaying *newHeader;
    sdword size, index, j;
    spaceobjpath *path;
    camerapath *cameraPath;
    Ship *newShip;
    vector xyz, xyzTemp, rot, nisPos;
    matrix coordsys, tempCoordsys;
    cameramotion *camPath;
    real32 length;
    nisevent *event;
    splinecurve *curve;

    // disable unit cap counting during NIS sequence
    unitCapDisable();

    //clear any possibly persistant game controls
    mrNISStarting();

    //allocate and initialize the header
    size = sizeof(nisplaying) + header->nObjectPaths * sizeof(objectmotion) +
        header->nCameraPaths * sizeof(cameramotion);
    newHeader = memAlloc(size, "NISPlaying", NonVolatile);
    newHeader->header = header;
    newHeader->timeElapsed = 0.0f;
    newHeader->nisMatrix = *coordSystem;
    newHeader->nisPosition = *position;
    newHeader->nObjects = header->nObjectPaths;
    //create pointers to motion paths from the same block of allocation
    newHeader->objectsInMotion = (objectmotion *)((ubyte *)newHeader + sizeof(nisplaying));
    newHeader->camerasInMotion = (cameramotion *)((ubyte *)newHeader->objectsInMotion + header->nObjectPaths * sizeof(objectmotion));
    newHeader->iCurrentEvent = 0;
    //initialize all the motion paths
    newHeader->objectSplines = memAlloc(sizeof(splinecurve) * 6 * newHeader->nObjects, "objSplinePool", NonVolatile);
    curve = newHeader->objectSplines;
    for (index = 0; index < newHeader->nObjects; index++)
    {
        path = &header->objectPath[index];
        bitClear(path->race, NIS_CentreShip);
        newHeader->objectsInMotion[index].flags = 0;
        newShip = nisNewObjectCreate(newHeader, index, header, path, newHeader, position);
        newHeader->objectsInMotion[index].spaceobj = (SpaceObjRotImp *)newShip;
        if (path->nSamples == NIS_OneKeyframe)
        {                                                   //if only one keyframe in motion path
            continue;
        }
        for (j = 0; j < 6; j++, curve++)
        {                                                   //for each of x,y,z,h,p,b
            newHeader->objectsInMotion[index].curve[j] = curve;
            bsCurveStartPrealloced(curve, path->nSamples, path->curve[j],
                    path->times, path->parameters);
        }
        newHeader->objectsInMotion[index].parentIndex = path->parentIndex;
    }
    newHeader->nCameras = header->nCameraPaths;
    newHeader->cameraSplines = memAlloc(sizeof(splinecurve) * 6 * newHeader->nCameras, "camSplinePool", NonVolatile);
    curve = newHeader->cameraSplines;
    for (index = 0; index < newHeader->nCameras; index++)
    {
        cameraPath = &header->cameraPath[index];
        newHeader->camerasInMotion[index].cam = memAlloc(sizeof(Camera), "NISCamera", NonVolatile);
        cameraInit(newHeader->camerasInMotion[index].cam, 1.0f);
        newHeader->camerasInMotion[index].cam->fieldofview = NIS_DefaultFOV;
        if (cameraPath->nSamples == NIS_OneKeyframe)
        {                                                   //if only one keyframe in motion path
            newHeader->camerasInMotion[index].curve[0] = NULL;//no motion path needed
            continue;
        }
        for (j = 0; j < 6; j++, curve++)
        {                                                   //for each of x,y,z,h,p,b
            newHeader->camerasInMotion[index].curve[j] = curve;
            bsCurveStartPrealloced(curve, cameraPath->nSamples, cameraPath->curve[j],
                    cameraPath->times, cameraPath->parameters);
        }
    }
    //!!! do the same for lights
    if (newHeader->nCameras > 0)
    {
        nisCamera = newHeader->camerasInMotion[0].cam;
        nisIsRunning = TRUE;
    }
#if NIS_RENDERLIST_DISABLE
    universe.dontUpdateRenderList = TRUE;                   //don't let automatic NIS updating go on
#endif

    if (RGL)
    {
        rglSuperClear();
    }

    nisTextCardIndex = 0;

    //initialize the NIS camera
    //get starting camera parameters
    camPath = &newHeader->camerasInMotion[0];
    nisPos = newHeader->nisPosition;                    //get position from NIS matrix
    xyz.x = camPath->curve[0]->points[0];
    xyz.y = camPath->curve[1]->points[0];
    xyz.z = camPath->curve[2]->points[0];
    rot.x = camPath->curve[3]->points[0];
    rot.y = camPath->curve[4]->points[0];
    rot.z = camPath->curve[5]->points[0];
    //compute starting parameter in world space
    //camera position: -Z, X, Y (UZI mode 33)
    if (mrCamera == NULL)
    {                                                   //make sure we've got a camera pointer
        mrCamera = nisCamera;
    }
    xyzTemp.x = -xyz.z;
    xyzTemp.y = xyz.x;
    xyzTemp.z = xyz.y;
    matMultiplyMatByVec(&xyz, &newHeader->nisMatrix, &xyzTemp);//multiply NIS world matrix
    camPath->cam->eyeposition.x = nisPos.x + xyz.x;
    camPath->cam->eyeposition.y = nisPos.y + xyz.y;
    camPath->cam->eyeposition.z = nisPos.z + xyz.z;
    nisShipEulerToMatrix(&tempCoordsys, &rot);
    matMultiplyMatByMat(&coordsys, &newHeader->nisMatrix, &tempCoordsys);//multiply NIS world matrix
    matGetVectFromMatrixCol2(xyz, coordsys);
    vecSub(xyzTemp, camPath->cam->eyeposition, mrCamera->eyeposition);
    length = getVectDistSloppy(xyzTemp) * NIS_CameraLengthFactor;
    vecMultiplyByScalar(xyz, length);

    matGetVectFromMatrixCol1(camPath->cam->upvector, coordsys);
    vecAdd(camPath->cam->lookatpoint, xyz, camPath->cam->eyeposition);
    //start the camera blend, if any
    if (nisCameraCutTime != 0.0f)
    {                                                       //if a camera fade-in was specified
        sdword endFrame;

        endFrame = 2;
        nisCameraParams[2] = nisCameraParams[3];            //prepare for a 1-phase camera cut
        if (nisCameraCutStage2 != -1.0f)
        {                                                   //if it's a 2-stage cut
            vector midEye, midLook, breakPoint, midLookPoint;
            real32 breakRadius;
            GrowSelection *triggerShips;

            triggerShips = kasGetGrowSelectionPtrIfExists("NisTriggerShips");
            if (triggerShips != NULL)
            {
                //compute the camera's eye and look points for the mid keyframe
                //such that it is looking past the trigger ships at the start point of the NIS
                nisFocusPointCompute(&breakPoint, (MaxSelection *)triggerShips->selection);
                breakRadius = nisFocusMaxRadius(&breakPoint, (MaxSelection *)triggerShips->selection);
                dbgAssert(breakRadius >= 0.0f && breakRadius < 14000);//make sure it's a reasonable size
                if (header->iLookyObject >= 0)
                {                                           //if some ship is the designated looky ship
                    path = &header->objectPath[header->iLookyObject];
                    //object position:-Z, X, Y (UZI position mode 33)
                    midLookPoint.x = -path->curve[2][0];
                    midLookPoint.y = path->curve[0][0];
                    midLookPoint.z = path->curve[1][0];
                    matMultiplyMatByVec(&xyz, &newHeader->nisMatrix, &midLookPoint);//multiply NIS world matrix
                    midLookPoint.x = nisPos.x + xyz.x;
                    midLookPoint.y = nisPos.y + xyz.y;
                    midLookPoint.z = nisPos.z + xyz.z;
                }
                else
                {                                           //else midpoint just looks at start of camera path
                    midLookPoint = camPath->cam->eyeposition;
                }
                vecSub(midLook, midLookPoint, breakPoint);  //vector from trigger ships to look point
                length = getVectDistSloppy(midLook) * NIS_CameraLengthFactor;//remember that distance
                vecNormalize(&midLook);                     //normalize that vector
                vecScalarMultiply(midEye, midLook, breakRadius * NIS_CameraBlendPullBackFactor);//and pull the camera back far enough to see the trigger ships
                vecSub(midEye, breakPoint, midEye);         //eye position in world space
                vecMultiplyByScalar(midLook, length);       //make the camera look far enough
                vecAdd(midLook, midEye, midLook);           //lookat point relative to eye

                nisCameraPoints[0][2] = midEye.x;
                nisCameraPoints[1][2] = midEye.y;
                nisCameraPoints[2][2] = midEye.z;
                nisCameraPoints[3][2] = midLook.x;
                nisCameraPoints[4][2] = midLook.y;
                nisCameraPoints[5][2] = midLook.z;
                nisCameraPoints[6][2] = 0.0f;               //up vector
                nisCameraPoints[7][2] = 0.0f;
                nisCameraPoints[8][2] = 1.0f;
                nisCameraTimes[2] = nisCameraCutTime;       //set time for mid keyframe
                nisCameraCutTime += nisCameraCutStage2;     //make nisCameraCutTime the total cut time
                nisCameraParams[2] = nisMiddleParam;
                endFrame = 3;                               //make room for the mid keyframe
            }
        }
        //set the start/end times and points
        nisCameraTimes[endFrame + 0] = nisCameraTimes[endFrame + 1] = nisCameraCutTime;
        nisCameraPoints[0][0] = nisCameraPoints[0][1] = mrCamera->eyeposition.x;
        nisCameraPoints[1][0] = nisCameraPoints[1][1] = mrCamera->eyeposition.y;
        nisCameraPoints[2][0] = nisCameraPoints[2][1] = mrCamera->eyeposition.z;
        nisCameraPoints[3][0] = nisCameraPoints[3][1] = mrCamera->lookatpoint.x;
        nisCameraPoints[4][0] = nisCameraPoints[4][1] = mrCamera->lookatpoint.y;
        nisCameraPoints[5][0] = nisCameraPoints[5][1] = mrCamera->lookatpoint.z;
        nisCameraPoints[6][0] = nisCameraPoints[6][1] = 0.0f;//up vector
        nisCameraPoints[7][0] = nisCameraPoints[7][1] = 0.0f;
        nisCameraPoints[8][0] = nisCameraPoints[8][1] = 1.0f;
        nisCameraPoints[0][endFrame + 0] = nisCameraPoints[0][endFrame + 1] = camPath->cam->eyeposition.x;
        nisCameraPoints[1][endFrame + 0] = nisCameraPoints[1][endFrame + 1] = camPath->cam->eyeposition.y;
        nisCameraPoints[2][endFrame + 0] = nisCameraPoints[2][endFrame + 1] = camPath->cam->eyeposition.z;
        nisCameraPoints[3][endFrame + 0] = nisCameraPoints[3][endFrame + 1] = camPath->cam->lookatpoint.x;
        nisCameraPoints[4][endFrame + 0] = nisCameraPoints[4][endFrame + 1] = camPath->cam->lookatpoint.y;
        nisCameraPoints[5][endFrame + 0] = nisCameraPoints[5][endFrame + 1] = camPath->cam->lookatpoint.z;
        nisCameraPoints[6][endFrame + 0] = nisCameraPoints[6][endFrame + 1] = camPath->cam->upvector.x;
        nisCameraPoints[7][endFrame + 0] = nisCameraPoints[7][endFrame + 1] = camPath->cam->upvector.y;
        nisCameraPoints[8][endFrame + 0] = nisCameraPoints[8][endFrame + 1] = camPath->cam->upvector.z;
        //start all the curves
        for (j = 0; j < 9; j++)
        {                 //!!! possible memory leak; these splines are never freed!!!
            nisCameraCurve[j] = bsCurveStart(endFrame + 2, nisCameraPoints[j], nisCameraTimes, nisCameraParams, FALSE);
        }
        //start the flight so that the camera is positioned correctly for the next rendered frame
        nisCameraFlyCompute(0.00001f);
    }
    if (mrCamera == NULL)
    {
        mrCamera = nisCamera;
    }
    else
    {
        nisPreviousCameraDistance = mrCamera->distance;
    }
    nisCaptureCamera = TRUE;                                //now using the NIS camera
    bCameraFocussed = FALSE;
    bPerfectFocusComputed = FALSE;

    if (!nisNewNISStarted)
    {                                                       //don't monkey volumes when linking to a new NIS
        soundEventGetVolume(&nisPreviousSFXVolume, &nisPreviousSpeechVolume, &nisPreviousMusicVolume);
    }

    if (!nisNewNISStarted)
    {
        nisBlackFade = nisBlackFadeDest = 0.0f;                 //no fade is the default
        nisTextScrollDistance = 0.0f;
        nisPreviousBackground = nisPreviousLighting = NULL;

    }

    trkTrackValueAdd("eye.x", &nisCamera->eyeposition.x, &newHeader->timeElapsed, colRGB(180, 100, 10));
    trkTrackValueAdd("eye.y", &nisCamera->eyeposition.y, &newHeader->timeElapsed, colRGB(180, 100, 10));
    trkTrackValueAdd("eye.z", &nisCamera->eyeposition.z, &newHeader->timeElapsed, colRGB(180, 100, 10));
    trkTrackValueAdd("look.x", &nisCamera->lookatpoint.x, &newHeader->timeElapsed, colRGB(100, 180, 10));
    trkTrackValueAdd("look.y", &nisCamera->lookatpoint.y, &newHeader->timeElapsed, colRGB(100, 180, 10));
    trkTrackValueAdd("look.z", &nisCamera->lookatpoint.z, &newHeader->timeElapsed, colRGB(100, 180, 10));

    nisUnivStartTime = universe.totaltimeelapsed;

    nisSMPTECounter = NULL;
    memset(nisStatic, 0, sizeof(nisStatic));
    //nisStatic = NULL;

    //execute any events at time 0
    event = header->events;
    while (newHeader->iCurrentEvent < newHeader->header->nEvents && event->time == 0.0f)
    {
        dbgAssert(nisEventDispatch[event->code] != NULL);
        nisEventDispatch[event->code](newHeader, event);
        newHeader->iCurrentEvent++;
        event++;
    }

    return(newHeader);
}

/*-----------------------------------------------------------------------------
    Name        : nisStop
    Description : Stop an NIS
    Inputs      : NIS - playing NIS to stop
    Outputs     :
    Return      : void
    Note        : Does not free the actual nisplaying structure
----------------------------------------------------------------------------*/
void nisStop(nisplaying *NIS)
{
    sdword index, j;
    Ship *ship;
    Derelict *derelict;
    real32 currentSFXVolume, currentSpeechVolume, currentMusicVolume;

    dbgAssert(NIS != NULL);
    for (index = 0; index < NIS->nObjects; index++)
    {                                                       //for each ship

        if (bitTest(NIS->objectsInMotion[index].flags, OMF_ObjectDied))
        {                                                   //don't free any objects that were deleted
            continue;
        }
        if (bitTest(NIS->objectsInMotion[index].flags, OMF_RemainAtEnd))
        {
            switch (NIS->header->objectPath[index].race)
            {
                case R1:                                    //any ship type
                case R2:
                case P1:
                case P2:
                case P3:
                case Traders:
                    ship = (Ship *)NIS->objectsInMotion[index].spaceobj;

                    ship->flags &= ~(SOF_DontApplyPhysics);
                    if (!bitTest(NIS->objectsInMotion[index].flags, OMF_KeepOnMoving))
                    {                                       //if we should stop this ship
                        vecZeroVector(ship->posinfo.velocity);
                        vecZeroVector(ship->posinfo.force);
                        vecZeroVector(ship->rotinfo.torque);
                        vecZeroVector(ship->rotinfo.rotspeed);
                        ship->posinfo.isMoving = FALSE;
                    }
                    ship->health = ship->staticinfo->maxhealth; //make it 'vincible'

                    unitCapEnable();
                    unitCapCreateShip(ship,ship->playerowner);
                    unitCapDisable();
                    break;
                case NSR_Asteroid:
                case NSR_DustCloud:
                case NSR_GasCloud:
                    ship = (Ship *)NIS->objectsInMotion[index].spaceobj;

                    ship->flags &= ~(SOF_DontApplyPhysics | SOF_NISShip);
                    vecZeroVector(ship->posinfo.velocity);
                    ship->posinfo.isMoving = FALSE;
                    ship = NULL;
                    break;
                case NSR_Derelict:
                    derelict = (Derelict *)NIS->objectsInMotion[index].spaceobj;
                    derelict->health = derelict->staticinfo->maxhealth;//make it 'vincible'
                    derelict->flags &= ~(SOF_DontApplyPhysics | SOF_NISShip);
                    vecZeroVector(derelict->posinfo.velocity);
                    derelict->posinfo.isMoving = FALSE;
                    ship = NULL;
                    break;
                case NSR_Effect:
                    ship = (Ship *)NIS->objectsInMotion[index].spaceobj;

                    ship->flags &= ~(SOF_DontApplyPhysics | SOF_NISShip);
                    vecZeroVector(ship->posinfo.velocity);
                    ship->posinfo.isMoving = FALSE;
                    ship = NULL;
                    break;
#if NIS_ERROR_CHECKING
                default:
                    dbgFatalf(DBG_Loc, "Don't know how to make race %d remainAtEnd.", NIS->header->objectPath[index].race);
#endif
            }
            if (ship != NULL)
            {
                bitClear(ship->flags, SOF_NISShip);
            }
        }
        else
        {
            switch (NIS->header->objectPath[index].race)
            {
                case R1:                                    //any ship type
                case R2:
                case P1:
                case P2:
                case P3:
                case Traders:
                    univReallyDeleteThisShipRightNow((Ship *)NIS->objectsInMotion[index].spaceobj);
                    break;
                case NSR_Asteroid:
                case NSR_DustCloud:
                case NSR_GasCloud:
                case NSR_Nebula:
                    univReallyDeleteThisResourceRightNow((Resource *)NIS->objectsInMotion[index].spaceobj);
                    break;
                case NSR_Derelict:
                    univReallyDeleteThisDerelictRightNow((Derelict *)NIS->objectsInMotion[index].spaceobj);
                    break;
                case NSR_Effect:
                    if (etgDeleteEffectsOwnedBy((Ship *)NIS->objectsInMotion[index].spaceobj) > 0)
                    {
#if NIS_VERBOSE_LEVEL > 2
                        dbgMessagef("\nNIS Effect not explicitly deleted.");
#endif
                    }
                    memFree(NIS->objectsInMotion[index].spaceobj);
                    break;
                case NSR_Generic:
                    univReallyDeleteThisDerelictRightNow((Derelict *)NIS->objectsInMotion[index].spaceobj);
                    break;
                case NSR_Missile:
                    ;                                       //don't bother deleting the missile because it'll just burn out and it may already have done so
                    break;
                default:;                                            //!!! add asteroids and other stuff
#if NIS_ERROR_CHECKING
                    dbgFatalf(DBG_Loc, "Undefined ship race for NIS spaceobj %d: %d.", index, NIS->header->objectPath[index].race);
#endif
            }
        }
        if (!bitTest(NIS->objectsInMotion[index].flags, OMF_CurvesDeleted))
        {
            for (j = 0; j < 6; j++)
            {                                                   //for each of x,y,z,h,p,b
                if (NIS->objectsInMotion[index].curve[j] != NULL)
                {                                               //delete all the spline curves that remain
                    //bsCurveDelete(NIS->objectsInMotion[index].curve[j]);
                }
                else
                {
                    break;
                }
            }
        }
    }
    memFree(NIS->objectSplines);                            //free the object spline pool
    if (!bCameraFocussed)
    {                                                       //if the script file didn't specify a focus point at the end
        //ccCopyCamera(&universe.mainCameraCommand,nisCamera);//focus the camera wherever it is
        nisCameraPrepareForCopy(nisCamera);
        nisCamera->oldlookatpoint = nisCamera->lookatpoint;
        nisCamera->distance = nisPreviousCameraDistance;
        cameraSetEyePosition(nisCamera);
        ccCopyCamera(&universe.mainCameraCommand,nisCamera);    //focus the camera from wherever it is

        if (universe.mainCameraCommand.currentCameraStack->focus.numShips)
        {
            ccFocusGeneral(&universe.mainCameraCommand, &universe.mainCameraCommand.currentCameraStack->focus, FALSE);
        }
        else
        {
            ccFocusOnMyMothership(&universe.mainCameraCommand);
        }
    }
    for (index = 0; index < NIS->nCameras; index++)
    {                                                       //for each camera
        memFree(NIS->camerasInMotion[index].cam);           //kill the camera
        for (j = 0; j < 6; j++)
        {                                                   //for each of x,y,z,h,p,b
            if (NIS->camerasInMotion[index].curve[j] != NULL)
            {                                               //delete all the spline curves that remain
                //bsCurveDelete(NIS->camerasInMotion[index].curve[j]);
            }
            else
            {
                break;
            }
        }
    }
    memFree(NIS->cameraSplines);

    nisIsRunning = FALSE;
    nisCaptureCamera = FALSE;
    nisCamera = NULL;

    if (nisMusicPlaying)
    {                                                       //if any music is playing
        if (universe.totaltimeelapsed - nisUnivStartTime < NIS->timeElapsed * NIS_PlayedProperlyMargin)
        {                                                   //if it was fast forwarded
            soundEventStopMusic(0.0f);                      //stop it
        }
        nisMusicPlaying = FALSE;
    }

    //!!! free any lights
#if NIS_RENDERLIST_DISABLE
    universe.dontUpdateRenderList = FALSE;                  //let automatic NIS updating go on
#endif
    memFree(NIS);

    nisTextCardIndex = 0;

    //reset any possibly persistent game controls
    mrNISStopping();

    // enable unit cap counting after NIS finished
    unitCapEnable();

    if (!nisNewNISStarted)
    {
        soundEventGetVolume(&currentSFXVolume, &currentSpeechVolume, &currentMusicVolume);
        if (currentMusicVolume != nisPreviousMusicVolume)
        {
#if NIS_VERBOSE_LEVEL > 2
            dbgMessagef("\nResetting Music volume from %.2f to %.2f", currentMusicVolume, nisPreviousMusicVolume);
#endif
            soundEventMusicVol(nisPreviousMusicVolume);
        }
        if (currentSpeechVolume != nisPreviousSpeechVolume)
        {
#if NIS_VERBOSE_LEVEL > 2
            dbgMessagef("\nResetting Speech volume from %.2f to %.2f", currentSpeechVolume, nisPreviousSpeechVolume);
#endif
            soundEventSpeechVol(nisPreviousSpeechVolume);
        }
        if (currentSFXVolume != nisPreviousSFXVolume)
        {
#if NIS_VERBOSE_LEVEL > 2
            dbgMessagef("\nResetting SFX volume from %.2f to %.2f", currentSFXVolume, nisPreviousSFXVolume);
#endif
            soundEventSFXVol(nisPreviousSFXVolume);
        }
        //restore previous lighting and backgrounds, if applicable
        if (nisPreviousLighting != NULL)
        {
            lightParseHSF(nisPreviousLighting);
            memFree(nisPreviousLighting);
        }
        if (nisPreviousBackground != NULL)
        {
            btgSetTheta(nisPreviousTheta);
            btgSetPhi(nisPreviousPhi);
            btgLoad(nisPreviousBackground);
            memFree(nisPreviousBackground);
        }

        nisBlackFade = nisBlackFadeDest = 0.0f;             //no fade is the default
        nisTextScrollDistance = 0.0f;

        if (nisUniverseHidden)
        {
            nisUniverseHideToggle(NULL, NULL);
        }
        if (nisUniversePause)
        {
            nisUniversePauseToggle(NULL, NULL);
        }
    }
    trkTrackValueRemoveAll();

    nisSMPTECounter = NULL;
    //nisStatic = NULL;
    memset(nisStatic, 0, sizeof(nisStatic));

}

/*-----------------------------------------------------------------------------
    Name        : nisSeek
    Description : Seek to an absolute point in an NIS.
    Inputs      : NIS - currently playing NIS to seek in.
                  seekTime - point to seek to, in seconds
    Outputs     : restarts all curves
    Return      :
----------------------------------------------------------------------------*/
#if NIS_SEEKABLE
void nisSeek(nisplaying *NIS, real32 seekTime)
{
    sdword index, j;
    objectmotion *path;
    cameramotion *camPath;
//    nisevent *event;
    real32 currentPos[6];
    Ship *newShip;
    vector position, startVector;
    real32 seekAmount;
    bool seekRelative;
    splinecurve *curve;
/*
    vector xyz, xyzTemp, rot;
    matrix coordsys, tempCoordsys;
*/
    dbgAssert(seekTime >= 0);
    dbgAssert(seekTime < NIS->header->length);

    if (seekTime > NIS->timeElapsed)
    {
        seekRelative = TRUE;
        seekAmount = seekTime - NIS->timeElapsed;
    }
    else
    {
        seekRelative = FALSE;
    }

    position = NIS->nisPosition;                            //get position from NIS matrix
    //reset all the object motion paths
    for (index = 0; index < NIS->header->nObjectPaths; index++)
    {
        path = &NIS->objectsInMotion[index];                //get pointer to object motion path
        if (path->curve[0] != NULL)
        {                                                   //if NULL motion path or NULL object
            for (j = 0; j < 6; j++)
            {                                               //for each motion channel
                if (seekRelative)
                {
                    bsCurveUpdate(path->curve[j], seekAmount);
                }
                else
                {
                    bsCurveRestart(path->curve[j]);         //restart the curve
                    bsCurveUpdate(path->curve[j], seekTime);//update to a good point
                }
            }
        }
        if (bitTest(path->flags, OMF_ObjectDied))
        {                                                   //if object had died
            newShip = nisNewObjectCreate(NIS, index, NIS->header, &NIS->header->objectPath[index], NIS, &position);
            NIS->objectsInMotion[index].spaceobj = (SpaceObjRotImp *)newShip;
            curve = &NIS->objectSplines[6 * index];
            if (NIS->header->objectPath[index].nSamples != NIS_OneKeyframe)
            {                                               //restart the motion curves
                for (j = 0; j < 6; j++, curve++)
                {                                           //for each of x,y,z,h,p,b
                    path->curve[j] = curve;
                    bsCurveStartPrealloced(curve, NIS->header->objectPath[index].nSamples, NIS->header->objectPath[index].curve[j],
                            NIS->header->objectPath[index].times, NIS->header->objectPath[index].parameters);
                    currentPos[j] = bsCurveUpdate(path->curve[j], seekTime);//update to a good point
                }
            }

            if (currentPos[j] != REALlyBig)
            {                                               //seek to the current position
                newShip->posinfo.position.x = position.x - currentPos[2];
                newShip->posinfo.position.y = position.y + currentPos[0];
                newShip->posinfo.position.z = position.z + currentPos[1];

                startVector.x = currentPos[3];
                startVector.y = currentPos[4];
                startVector.z = currentPos[5];
                nisShipEulerToMatrix(&newShip->rotinfo.coordsys, &startVector);

                univUpdateObjRotInfo((SpaceObjRot *)newShip);
            }
//            path->bObjectDied = FALSE;
            bitClear(path->flags, OMF_ObjectDied);
        }
    }
    //reset all the camera motion paths
    for (index = 0; index < NIS->header->nCameraPaths; index++)
    {
        camPath = &NIS->camerasInMotion[index];             //get pointer to camera motion path
        if (camPath->curve[0] == NULL)
        {                                                   //if NULL camera motion path
            continue;                                       //leave this camera where it was
        }
        for (j = 0; j < 6; j++)
        {                                                   //for each motion channel
            if (seekRelative)
            {
                bsCurveUpdate(camPath->curve[j], seekAmount); //update to a good point
            }
            else
            {
                bsCurveRestart(camPath->curve[j]);          //restart the curve
                bsCurveUpdate(camPath->curve[j], seekTime); //update to a good point
            }
        }
    }
    //seek to the current event
    if (!seekRelative)
    {
        NIS->iCurrentEvent = 0;
    }
    bitSet(NIS->header->flags, NIS_Seeked);
    /*
    event = &NIS->header->events[NIS->iCurrentEvent];
    nisSeeking = TRUE;
    while (NIS->iCurrentEvent < NIS->header->nEvents && event->time <= seekTime)
    {
        dbgAssert(nisEventDispatch[event->code] != NULL);
        if (event->code == NEO_Focus)
        {                                                   //special case for camera focus:rewind camera to the focus point
            camPath = &NIS->camerasInMotion[0];             //get pointer to camera motion path
            dbgAssert(camPath->curve[0] != NULL);
            for (j = 0; j < 6; j++)
            {                                               //for each motion channel
                bsCurveRestart(camPath->curve[j]);          //restart the curve
            }
            xyz.x = bsCurveUpdate(camPath->curve[0], event->time); //update the first curve
            //this chunk of code checks if the NIS loops
            dbgAssert(xyz.x != REALlyBig);                  //if curve ends
            dbgAssert(!_isnan((double)xyz.x));
            //get rest of motion path
            xyz.y = bsCurveUpdate(camPath->curve[1], event->time);
            xyz.z = bsCurveUpdate(camPath->curve[2], event->time);
            dbgAssert(!_isnan((double)xyz.y));
            dbgAssert(!_isnan((double)xyz.z));
            //camera rotation: X, Y, Z (mode 0)
            rot.x = bsCurveUpdate(camPath->curve[3], event->time);
            rot.y = bsCurveUpdate(camPath->curve[4], event->time);
            rot.z = bsCurveUpdate(camPath->curve[5], event->time);
            dbgAssert(xyz.y != REALlyBig && xyz.z != REALlyBig && rot.x != REALlyBig && rot.y != REALlyBig && rot.z != REALlyBig);
            //camera position: -Z, X, Y (UZI mode 33)
            xyzTemp.x = -xyz.z;
            xyzTemp.y = xyz.x;
            xyzTemp.z = xyz.y;
            dbgAssert(!_isnan((double)xyzTemp.x));
            dbgAssert(!_isnan((double)xyzTemp.y));
            dbgAssert(!_isnan((double)xyzTemp.z));
            matMultiplyMatByVec(&xyz, &NIS->nisMatrix, &xyzTemp);//multiply NIS world matrix
            camPath->cam->eyeposition.x = NIS->nisPosition.x + xyz.x;
            camPath->cam->eyeposition.y = NIS->nisPosition.y + xyz.y;
            camPath->cam->eyeposition.z = NIS->nisPosition.z + xyz.z;
            nisShipEulerToMatrix(&tempCoordsys, &rot);
            matMultiplyMatByMat(&coordsys, &NIS->nisMatrix, &tempCoordsys);//multiply NIS world matrix
            matGetVectFromMatrixCol2(xyz, coordsys);
            matGetVectFromMatrixCol1(camPath->cam->upvector, coordsys);
            vecAdd(camPath->cam->lookatpoint, xyz, camPath->cam->eyeposition);
        }
        nisEventDispatch[event->code](NIS, event);
        NIS->iCurrentEvent++;
        event++;
    }
    nisSeeking = FALSE;
        */
    //!!! reset all the light motion paths
    NIS->timeElapsed = seekTime;
}
#endif

/*-----------------------------------------------------------------------------
    Name        : nisGoToEnd
    Description : Causes NIS to go to end of NIS
    Inputs      : NIS
    Outputs     :
    Return      :
----------------------------------------------------------------------------*/
void nisGoToEnd(nisplaying *NIS)
{
    nisSeek(NIS,NIS->header->length - UNIVERSE_UPDATE_PERIOD);
}

/*-----------------------------------------------------------------------------
    Name        : nisObjectHierarchyMatrixCompute
    Description : Compute the matrix for an NIS hierarchy object.
    Inputs      :
    Outputs     :
    Return      :
----------------------------------------------------------------------------*/
void nisObjectHierarchyMatrixCompute(objectmotion *paths, sdword index, matrix *mat, vector *vec)
{
    matrix temp;
    vector tempVec;
    if (paths[index].parentIndex != -1)
    {
        nisObjectHierarchyMatrixCompute(paths, paths[index].parentIndex, &temp, &tempVec);
        matMultiplyMatByMat(mat, &paths[index].spaceobj->rotinfo.coordsys, &temp);
        vecAdd(*vec, paths[index].spaceobj->posinfo.position, tempVec);
    }
    else
    {
        *mat = paths[index].spaceobj->rotinfo.coordsys;
        *vec = paths[index].spaceobj->posinfo.position;
    }
}

/*-----------------------------------------------------------------------------
    Name        : nisPause
    Description : Pause playback of an NIS
    Inputs      : bPause - TRUE if you want to pause an NIS
    Outputs     :
    Return      :
----------------------------------------------------------------------------*/
void nisPause(bool bPause)
{
    nisPaused = bPause;
}

/*-----------------------------------------------------------------------------
    Name        : nisCamPathUpdate
    Description : Update the camera motion for a given camera.
    Inputs      : NIS - what NIS this camera path belongs to
                  camPath - camera motion info
                  timeElapsed - relative amount to advance motion path
    Outputs     :
    Return      : void
----------------------------------------------------------------------------*/
void nisCamPathUpdate(nisplaying *NIS, cameramotion *camPath, real32 timeElapsed)
{
    vector xyz, xyzTemp, rot;
    matrix coordsys, tempCoordsys;

    dbgAssert(camPath->curve[0] != NULL);
    xyz.x = bsCurveUpdate(camPath->curve[0], timeElapsed); //update the first curve
    //this chunk of code checks if the NIS loops
//    dbgAssert(xyz.x != REALlyBig);                  //if curve ends
    if (xyz.x == REALlyBig)                             //if curve ends
    {                                                   //(all curves should end at same time)
        return;
    }
    dbgAssert(!_isnan((double)xyz.x));
    //get rest of motion path
    xyz.y = bsCurveUpdate(camPath->curve[1], timeElapsed);
    xyz.z = bsCurveUpdate(camPath->curve[2], timeElapsed);
    dbgAssert(!_isnan((double)xyz.y));
    dbgAssert(!_isnan((double)xyz.z));
    //camera rotation: X, Y, Z (mode 0)
    rot.x = bsCurveUpdate(camPath->curve[3], timeElapsed);
    rot.y = bsCurveUpdate(camPath->curve[4], timeElapsed);
    rot.z = bsCurveUpdate(camPath->curve[5], timeElapsed);
    dbgAssert(xyz.y != REALlyBig && xyz.z != REALlyBig && rot.x != REALlyBig && rot.y != REALlyBig && rot.z != REALlyBig);
    //camera position: -Z, X, Y (UZI mode 33)
    xyzTemp.x = -xyz.z;
    xyzTemp.y = xyz.x;
    xyzTemp.z = xyz.y;
    dbgAssert(!_isnan((double)xyzTemp.x));
    dbgAssert(!_isnan((double)xyzTemp.y));
    dbgAssert(!_isnan((double)xyzTemp.z));
    matMultiplyMatByVec(&xyz, &NIS->nisMatrix, &xyzTemp);//multiply NIS world matrix
    camPath->cam->eyeposition.x = NIS->nisPosition.x + xyz.x;
    camPath->cam->eyeposition.y = NIS->nisPosition.y + xyz.y;
    camPath->cam->eyeposition.z = NIS->nisPosition.z + xyz.z;
    nisShipEulerToMatrix(&tempCoordsys, &rot);
    matMultiplyMatByMat(&coordsys, &NIS->nisMatrix, &tempCoordsys);//multiply NIS world matrix
    matGetVectFromMatrixCol2(xyz, coordsys);
    vecMultiplyByScalar(xyz, NIS_LookScalar);
    matGetVectFromMatrixCol1(camPath->cam->upvector, coordsys);
    vecAdd(camPath->cam->lookatpoint, xyz, camPath->cam->eyeposition);
#if NIS_VERBOSE_LEVEL >= 4
        dbgMessagef("\nCamera: (%.2f, %.2f, %.2f) angles (%.2f, %.2f, %.2f), (%.2f, %.2f, %.2f).", camPath->cam->eyeposition.x, camPath->cam->eyeposition.y, camPath->cam->eyeposition.z, rot.x, rot.y, rot.z, xyz.x, xyz.y, xyz.z);
#endif//NIS_VERBOSE_LEVEL
}

/*-----------------------------------------------------------------------------
    Name        : nisUpdate
    Description : Update an NIS for a given time interval
    Inputs      : NIS - nis to update
                  timeElapsed - time since NIS last updated or created
    Outputs     : updates all the splines in the NIS
    Return      : Total time elapsed in the NIS, or REALlyBig if it's all done.
----------------------------------------------------------------------------*/
real32 nisUpdate(nisplaying *NIS, real32 timeElapsed)
{
    vector xyz, xyzTemp, rot, nisPos, tempVect, lastPos;
    matrix coordsys, tempCoordsys;//, temp;
    objectmotion *path;
    cameramotion *camPath;
    sdword index, j;
    nisevent *event;

    if (!binkDonePlaying)
    {
        return 0.0000001f;
    }

#if NIS_CAMERA_RELEASE
    if (keyIsStuck(NIS_CAMERA_RELEASE))
    {
        keyClearSticky(NIS_CAMERA_RELEASE);
        nisCaptureCamera ^= TRUE;
    }
#endif

#if NIS_TIME_CONTROLS
    if (keyIsStuck(NIS_Pause))
    {                                                       //pause/unpause the game
        keyClearSticky(NIS_Pause);
        nisPaused ^= TRUE;
        dbgMessagef("\nNIS playback %s", nisPaused ? "PAUSED" : "UNPAUSED");
    }
#if NIS_SEEKABLE
    if (nisPaused)
    {
        timeElapsed = 0.0000001f;
    }
    if (keyIsStuck(NIS_Back1Second))
    {
        keyClearSticky(NIS_Back1Second);
        nisSeek(NIS, max(0, NIS->timeElapsed - 1.0f));
        dbgMessagef("\nNIS seek backward 1 second.");
    }
    if (keyIsStuck(NIS_Back10Second))
    {
        keyClearSticky(NIS_Back10Second);
        nisSeek(NIS, max(0, NIS->timeElapsed - 10.0f));
        dbgMessagef("\nNIS seek backward 10 seconds.");
    }
#endif
    if (keyIsStuck(NIS_FastForward))
    {
        keyClearSticky(NIS_FastForward);
        if (keyIsHit(CONTROLKEY))
        {
            nisPlayFactor /= 2.0f;
        }
        else
        {
            nisPlayFactor *= 2.0f;
        }
        dbgMessagef("\nNIS playback speed x%.2f.", nisPlayFactor);
    }
    if (keyIsStuck(NIS_NormalSpeed))
    {
        keyClearSticky(NIS_NormalSpeed);
        nisPlayFactor = 1.0f;
        dbgMessagef("\nNIS playback NORMAL speed.", nisPlayFactor);
    }

    timeElapsed *= nisPlayFactor;
#endif
    nisPos = NIS->nisPosition;                              //get position from NIS matrix
    for (index = 0; index < NIS->nObjects; index++)
    {                                                       //for each object in motion NIS
        path = &NIS->objectsInMotion[index];                //get pointer to object motion path
        if (bitTest(path->flags, OMF_CurvesDeleted))
        {                                                   //if the object is already dead
            continue;
        }
        if (path->curve[0] == NULL)
        {                                                   //if NULL motion path or NULL object
            path->spaceobj->posinfo.isMoving = FALSE;
            continue;                                       //leave this object where it was
        }
        xyz.x = bsCurveUpdate(path->curve[0], timeElapsed); //update the first curve
        //this chunk of code checks if the NIS loops
        if (xyz.x == REALlyBig)                             //if curve ends
        {                                                   //(all curves should end at same time)
            path->spaceobj->posinfo.isMoving = FALSE;
            continue;                                       //don't update this one anymore
        }
        //get rest of motion path
        xyz.y = bsCurveUpdate(path->curve[1], timeElapsed);
        xyz.z = bsCurveUpdate(path->curve[2], timeElapsed);
        rot.x = bsCurveUpdate(path->curve[3], timeElapsed);
        rot.y = bsCurveUpdate(path->curve[4], timeElapsed);
        rot.z = bsCurveUpdate(path->curve[5], timeElapsed);
        //verify none of these curves expired
        dbgAssert(xyz.y != REALlyBig && xyz.z != REALlyBig && rot.x != REALlyBig && rot.y != REALlyBig && rot.z != REALlyBig);

        nisShipEulerToMatrix(&tempCoordsys, &rot);          //multiply NIS world matrix
        matMultiplyMatByMat(&coordsys, &NIS->nisMatrix, &tempCoordsys);
        //stuff the new coordinate system and position into the spaceobj
        lastPos = path->spaceobj->posinfo.position;
        //object position:-Z, X, Y (UZI position mode 33)
        xyzTemp.x = -xyz.z;
        xyzTemp.y = xyz.x;
        xyzTemp.z = xyz.y;
        matMultiplyMatByVec(&xyz, &NIS->nisMatrix, &xyzTemp);//multiply NIS world matrix
        path->spaceobj->posinfo.position.x = nisPos.x + xyz.x;
        path->spaceobj->posinfo.position.y = nisPos.y + xyz.y;
        path->spaceobj->posinfo.position.z = nisPos.z + xyz.z;
        path->spaceobj->rotinfo.coordsys = coordsys;        //set new object coordinate system

        //get velocity of ship
        vecSub(tempVect, path->spaceobj->posinfo.position, lastPos);
        //kill the trails if the ship has jumped to new queue point
        if (path->spaceobj->objtype == OBJ_ShipType &&
            getVectDistSloppy(tempVect) >= NIS_JumpDistance)
        {
            for (j = 0; j < MAX_NUM_TRAILS; j++)
            {
                if (((Ship*)path->spaceobj)->trail[j] != NULL)
                {
                    trailZeroLength(((Ship*)path->spaceobj)->trail[j]);
                }
            }
        }
        //cap the velocity vector of the ship
        if (abs(tempVect.x) + abs(tempVect.y) + abs(tempVect.z) > 0.0f)
        {
            if (NIS->header->objectPath[index].type < NUM_RACES)
            {
                vecCapVector(&tempVect, ((Ship *)path->spaceobj)->staticinfo->staticheader.maxvelocity);
            }
            path->spaceobj->posinfo.isMoving = TRUE;
        }
        else
        {
            path->spaceobj->posinfo.isMoving = FALSE;
        }
        path->spaceobj->posinfo.velocity = tempVect;
        if (path->spaceobj->flags & SOF_Rotatable)
        {
            univUpdateObjRotInfo((SpaceObjRot *)path->spaceobj);
        }
#if NIS_VERBOSE_LEVEL >= 4
        dbgMessagef("\nNIS object at 0x%x at (%.2f, %.2f, %.2f) angles (%.2f, %.2f, %.2f).", path->spaceobj, path->spaceobj->posinfo.position.x, path->spaceobj->posinfo.position.y, path->spaceobj->posinfo.position.z, rot.x, rot.y, rot.z);
#endif
    }
    for (index = 0; index < NIS->nCameras; index++)
    {                                                       //for each camera motion NIS
        camPath = &NIS->camerasInMotion[index];             //get pointer to object motion camPath
        if (camPath->curve[0] == NULL)
        {                                                   //if NULL motion camPath
            continue;                                       //leave this object where it was
        }
        nisCamPathUpdate(NIS, camPath, timeElapsed);
/*
        xyz.x = bsCurveUpdate(camPath->curve[0], timeElapsed); //update the first curve
        //this chunk of code checks if the NIS loops
        if (xyz.x == REALlyBig)                             //if curve ends
        {                                                   //(all curves should end at same time)
            continue;
        }
        dbgAssert(!_isnan((double)xyz.x));
        //get rest of motion path
        xyz.y = bsCurveUpdate(camPath->curve[1], timeElapsed);
        xyz.z = bsCurveUpdate(camPath->curve[2], timeElapsed);
        dbgAssert(!_isnan((double)xyz.y));
        dbgAssert(!_isnan((double)xyz.z));
        //camera rotation: X, Y, Z (mode 0)
        rot.x = bsCurveUpdate(camPath->curve[3], timeElapsed);
        rot.y = bsCurveUpdate(camPath->curve[4], timeElapsed);
        rot.z = bsCurveUpdate(camPath->curve[5], timeElapsed);
        dbgAssert(xyz.y != REALlyBig && xyz.z != REALlyBig && rot.x != REALlyBig && rot.y != REALlyBig && rot.z != REALlyBig);
        //camera position: -Z, X, Y (UZI mode 33)
        xyzTemp.x = -xyz.z;
        xyzTemp.y = xyz.x;
        xyzTemp.z = xyz.y;
        dbgAssert(!_isnan((double)xyzTemp.x));
        dbgAssert(!_isnan((double)xyzTemp.y));
        dbgAssert(!_isnan((double)xyzTemp.z));
        matMultiplyMatByVec(&xyz, &NIS->nisMatrix, &xyzTemp);//multiply NIS world matrix
        camPath->cam->eyeposition.x = nisPos.x + xyz.x;
        camPath->cam->eyeposition.y = nisPos.y + xyz.y;
        camPath->cam->eyeposition.z = nisPos.z + xyz.z;
        nisShipEulerToMatrix(&tempCoordsys, &rot);
        matMultiplyMatByMat(&coordsys, &NIS->nisMatrix, &tempCoordsys);//multiply NIS world matrix
        matGetVectFromMatrixCol2(xyz, coordsys);
        matGetVectFromMatrixCol1(camPath->cam->upvector, coordsys);
        vecAdd(camPath->cam->lookatpoint, xyz, camPath->cam->eyeposition);
#if NIS_VERBOSE_LEVEL >= 4
        dbgMessagef("\nCamera: (%.2f, %.2f, %.2f) angles (%.2f, %.2f, %.2f), (%.2f, %.2f, %.2f).", camPath->cam->eyeposition.x, camPath->cam->eyeposition.y, camPath->cam->eyeposition.z, rot.x, rot.y, rot.z, xyz.x, xyz.y, xyz.z);
#endif//NIS_VERBOSE_LEVEL
*/
    }

    //process all the events pending
    event = &NIS->header->events[NIS->iCurrentEvent];
    if (bitTest(NIS->header->flags, NIS_Seeked))
    {                                                       //if we have to seek some events
        bitClear(NIS->header->flags, NIS_Seeked);
        nisSeeking = TRUE;
        while (NIS->iCurrentEvent < NIS->header->nEvents && event->time <= NIS->timeElapsed)
        {                                                   //execute all seekable events pending
            dbgAssert(nisEventDispatch[event->code] != NULL);
            nisEventDispatch[event->code](NIS, event);
            if (nisNewNISStarted)
            {
                nisNewNISStarted = FALSE;
                return(0.0f);
            }
            NIS->iCurrentEvent++;
            event++;
        }
        nisSeeking = FALSE;
    }
    //now process the regular events pending
    while (NIS->iCurrentEvent < NIS->header->nEvents && event->time <= NIS->timeElapsed + timeElapsed)
    {
        dbgAssert(nisEventDispatch[event->code] != NULL);
        nisEventDispatch[event->code](NIS, event);
        if (nisNewNISStarted)
        {
            nisNewNISStarted = FALSE;
            return(0.0f);
        }
        NIS->iCurrentEvent++;
        event++;
    }
#if NIS_PRINT_INFO
    if (keyIsStuck(NIS_PRINT_INFO))
    {                                                       //toggle time display
        keyClearSticky(NIS_PRINT_INFO);
        nisPrintInfo ^= TRUE;
        dbgMessagef("\nNIS info printing %s", nisPrintInfo ? "ON" : "OFF");
    }
    if (nisPrintInfo)
    {
        sprintf(nisInfoString, "NIS time = %.2f of %.2f, frame = %.0f of %.0f", NIS->timeElapsed, NIS->header->length, NIS->timeElapsed * NIS_FrameRate, NIS->header->length * NIS_FrameRate);
        if (selSelected.numShips == 1)
        {                                                   //print index of selected ship
            char string[256], *pString;

            for (index = 0; index < NIS->nObjects; index++)
            {                                               //for each object in motion NIS
                if ((Ship *)NIS->objectsInMotion[index].spaceobj == selSelected.ShipPtr[0])
                {
                    strcat(nisInfoString, " Selected: '");
                    if (NIS->objectsInMotion[index].spaceobj->objtype == OBJ_ShipType)
                    {                                       //if it's a ship
                        pString = NisRaceToStr(((Ship *)NIS->objectsInMotion[index].spaceobj)->shiprace);
                        if (pString != NULL)
                        {
                            strcat(nisInfoString, pString);
                            strcat(nisInfoString, "\\");
                        }
                    }
                    strcat(nisInfoString, ShipTypeToStr(((Ship *)NIS->objectsInMotion[index].spaceobj)->shiptype));
                    if (NIS->header->objectPath[index].instance != 0)
                    {
                        sprintf(string, " (%d)", NIS->header->objectPath[index].instance);
                        strcat(nisInfoString, string);
                    }
                    strcat(nisInfoString, "'");
                    break;
                }
            }
        }
    }
#endif
    //update the time of the NIS and restart it if it ends
    NIS->timeElapsed += timeElapsed;
    if (NIS->timeElapsed >= NIS->header->length)
    {                                                       //if NIS times out
#if NIS_SEEKABLE
        if (NIS->header->loop >= 0)
        {                                                   //if we should loop this NIS
            nisSeek(NIS, NIS->header->loop);
        }
        else
#endif
        {
            return(REALlyBig);
        }
    }
    return(NIS->timeElapsed);
}

/*-----------------------------------------------------------------------------
    Name        : nisDeleteTheseCurves
    Description : Delete this NIS-ship and it's motion paths
    Inputs      :
    Outputs     :
    Return      :
----------------------------------------------------------------------------*/
void nisDeleteTheseCurves(objectmotion *path)
{
    sdword j;

    for (j = 0; j < 6; j++)
    {                                                   //for each of x,y,z,h,p,b
        if (path->curve[j] != NULL)
        {                                               //delete all the spline curves that remain
            //bsCurveDelete(path->curve[j]);
            path->curve[j] = NULL;
        }
    }
    path->flags |= OMF_CurvesDeleted;
}


/*-----------------------------------------------------------------------------
    Name        : nisFindObjectByName
    Description : Find the currently named object in the specified NIS.
    Inputs      : header - NIS to look through.
                  name - name of object in the format:
                    "[<race>\]<lwoFileName>[ (<instance>)]"
    Outputs     :
    Return      : Index of object.
----------------------------------------------------------------------------*/
sdword nisFindObjectByName(nisheader *header, char *name)
{
    sdword index, instance = 0;
    udword race = R1;
//    ShipType type;
    char *pSlash, *pType;
    char raceString[32];
    char effectName[256];
    spaceobjpath *path;

    if ((pSlash = strchr(name, '(')) != NULL)
    {                                                       //find the instance ID, if any
        sscanf(pSlash, "(%d)", &instance);
        *(pSlash - 1) = 0;                                  //get rid of the instance ID
    }
    if ((pSlash = strchr(name, '\\')) != NULL || (pSlash = strchr(name, '/')) != NULL)
    {                                                       //find the race
        memcpy(raceString, name, pSlash - name);
        raceString[pSlash - name] = 0;
        race = StrToNisRace(raceString);
        if (bitTest(header->flags, NHF_RacesSwapped))
        {
            if (race == R1)
            {
                race = R2;
            }
            else if (race == R2)
            {
                race = R1;
            }
        }
        dbgAssert(race != -1);
        pSlash++;                                           //point to first character of object name
    }
    else
    {
        pSlash = name;
    }
    if (!_stricmp(pSlash, "Missile"))
    {                                                       //if we're looking for a missile
        race = NSR_Missile;
    }
//    type = StrToShipType(pSlash);                           //get the type

    path = header->objectPath;
    for (index = 0; index < header->nObjectPaths; index++, path++)
    {                                                       //look at all object paths
        /*
        if (path->instance == instance && path->race == race && path->type == type)
        {                                                   //if matching object criteria
            return(index);
        }
        */
        if (path->instance == instance && path->race == race)
        {                                                   //if instance and race match
            switch (race)
            {
                case R1:                                    //any ship type
                case R2:
                case P1:
                case P2:
                case P3:
                case Traders:
                    pType = ShipTypeToStr(path->type);
                    break;
                case NSR_Asteroid:
                    pType = AsteroidTypeToStr(path->type);
                    break;
                case NSR_DustCloud:
                    pType = DustCloudTypeToStr(path->type);
                    break;
                case NSR_GasCloud:
                    pType = GasCloudTypeToStr(path->type);
                    break;
                case NSR_Nebula:
                    pType = NebulaTypeToStr(path->type);
                    break;
                case NSR_Derelict:
                    pType = DerelictTypeToStr(path->type);
                    break;
                case NSR_Effect:
                    for (pType = ((etgeffectstatic *)path->type)->name; strchr(pType, '\\'); pType = strchr(pType, '\\'))
                    {                                       //find just the name
                        ;
                    }
                    strcpy(effectName, pType);              //copy over just the name
                    pType = strchr(effectName, '.');
                    if (pType != NULL)                      //remove the extension
                    {
                        *pType = 0;
                    }
                    pType = effectName;
                    break;
                case NSR_Generic:
                    pType = header->genericObject[path->type]->name;
//                    pType = (char *)path->type;             //!!!
                    break;
                case NSR_Missile:
                    pType = ShipRaceToStr(path->type);      //get a race string
                    break;
                default:;
#if NIS_ERROR_CHECKING
                    dbgFatalf(DBG_Loc, "Invalid object race: %d", path->race);
#endif
            }
            if (!_stricmp(pType, pSlash))
            {                                               //if this is the proper object type
                return(index);
            }
        }
    }
#if NIS_ERROR_CHECKING
    dbgFatalf(DBG_Loc, "nisFindObjectByName: object '%s' not found.", name);
#endif
    return(-1);
}

/*-----------------------------------------------------------------------------
    Name        : nisObjectDied
    Description : Ship death function called from univupdate when objects die.
    Inputs      : NIS - what NIS we're removing references from
                  obj - object that is expiring
    Outputs     :
    Return      :
----------------------------------------------------------------------------*/
void nisRemoveObjectReference(nisplaying *NIS, SpaceObj *obj)
{
    sdword index;

    dbgAssert(NIS != NULL);
    for (index = 0; index < NIS->nObjects; index++)
    {
        if (NIS->objectsInMotion[index].spaceobj == (SpaceObjRotImp *)obj)
        {                                                   //if this is the object that is dying
            if (!(NIS->objectsInMotion[index].flags & OMF_CurvesDeleted))
            {                                               //and it's still not totally dead
                if (NIS->header->objectPath[index].nSamples != NIS_OneKeyframe)
                {                                           //and there is an actual motion curve
                    nisDeleteTheseCurves(&NIS->objectsInMotion[index]);//stop animating it
                }
                else
                {
                    bitSet(NIS->objectsInMotion[index].flags, OMF_CurvesDeleted);
                }
            }
            NIS->objectsInMotion[index].spaceobj = NULL;
            break;
        }
    }
}

/*-----------------------------------------------------------------------------
    Name        : nisObjectDied
    Description : Remove references from a particular object from any playing NIS's
    Inputs      : obj - object that is expiring
    Outputs     :
    Return      :
----------------------------------------------------------------------------*/
void nisObjectDied(SpaceObj *obj)
{
    if (testPlaying != NULL)
    {
        nisRemoveObjectReference(testPlaying, obj);
    }
    if (thisNisPlaying != NULL)
    {
        nisRemoveObjectReference(thisNisPlaying, obj);
    }

}

/*-----------------------------------------------------------------------------
    Name        : nisRemoveMissileReferenceFromPlaying
    Description : Removed missile referenced from a particular playing NIS
    Inputs      : NIS - nis to search through
                  missile - missile to remove references of
    Outputs     :
    Return      : voiv
----------------------------------------------------------------------------*/
void nisRemoveMissileReferenceFromPlaying(nisplaying *NIS, Missile *missile)
{
    sdword index;

    for (index = 0; index < NIS->nObjects; index++)
    {                                                       //search all objects
        if ((Missile *)NIS->objectsInMotion[index].spaceobj == missile)
        {                                                   //if this is the one to delete
            dbgAssert(NIS->header->objectPath[index].race == NSR_Missile);//make sure it's a missile
            if (!bitTest(NIS->objectsInMotion[index].flags, OMF_ObjectDied))
            {                                               //if it hasn't died yet
                nisDeleteTheseCurves(&NIS->objectsInMotion[index]);
                bitSet(NIS->objectsInMotion[index].flags, OMF_ObjectDied);//make it die
                NIS->objectsInMotion[index].spaceobj = NULL;//kill the pointer
            }
        }
    }
}

/*-----------------------------------------------------------------------------
    Name        : nisRemoveMissileReference
    Description : Removes referenced to deleted missile, as the NIS may be
                    animating it.
    Inputs      : missile - missile that just died
    Outputs     :
    Return      : void
----------------------------------------------------------------------------*/
void nisRemoveMissileReference(Missile *missile)
{
    if (testPlaying != NULL)
    {
        nisRemoveMissileReferenceFromPlaying(testPlaying, missile);
    }
    if (thisNisPlaying != NULL)
    {
        nisRemoveMissileReferenceFromPlaying(thisNisPlaying, missile);
    }
}

/*-----------------------------------------------------------------------------
    Name        : nisDeatAnything
    Description : Event execution callback
    Inputs      :
    Outputs     :
    Return      :
----------------------------------------------------------------------------*/
void nisDeathAnything(nisplaying *NIS, nisevent *event)
{
    DeleteShip *deleteShip;
    Ship *deadShip = (Ship *)NIS->objectsInMotion[event->shipID].spaceobj;

    if (nisSeeking)
    {
        return;
    }
    if (bitTest(NIS->objectsInMotion[event->shipID].flags, OMF_ObjectDied))
    {                                                       //if ship already dead
        return;
    }

    if (deadShip->objtype == OBJ_ShipType)
    {
        deleteShip = memAlloc(sizeof(DeleteShip),"DeleteShip",Pyrophoric);
        deleteShip->ship = deadShip;
        deleteShip->deathBy = event->param[0];
        listAddNode(&universe.DeleteShipList,&(deleteShip->objlink),deleteShip);
    }
    else
    {
        dbgAssert(deadShip->objtype == OBJ_DerelictType);
        AddDerelictToDeleteDerelictList((Derelict *)deadShip, event->param[0]);
    }
    bitSet(NIS->objectsInMotion[event->shipID].flags, OMF_ObjectDied);
}
void nisAttack(nisplaying *NIS, nisevent *event)
{
    Ship *goodShip, *badShip;
    SelectCommand selectone;
    AttackCommand attack;

    goodShip = (Ship *)NIS->objectsInMotion[event->shipID].spaceobj;
    badShip = (Ship *)NIS->objectsInMotion[event->param[0]].spaceobj;
    selectone.numShips = 1;
    selectone.ShipPtr[0] = goodShip;
    attack.numTargets = 1;
    attack.TargetPtr[0] = (SpaceObjRotImpTarg *)badShip;
    if (goodShip->playerowner == badShip->playerowner)
    {
        clAttack(&universe.mainCommandLayer,&selectone,&attack);
    }
    else
    {
        clPassiveAttack(&universe.mainCommandLayer,&selectone,&attack);
    }
}
void nisHaltAttack(nisplaying *NIS, nisevent *event)
{
    Ship *goodShip;
    SelectCommand selectone;

    goodShip = (Ship *)NIS->objectsInMotion[event->shipID].spaceobj;
    selectone.numShips = 1;
    selectone.ShipPtr[0] = goodShip;
    RemoveShipsFromDoingStuff(&universe.mainCommandLayer,&selectone);
}
void nisFire(nisplaying *NIS, nisevent *event)
{
    Ship *goodShip;
    ShipStaticInfo *shipstatic;

    goodShip = (Ship *)NIS->objectsInMotion[event->shipID].spaceobj;
    shipstatic = (ShipStaticInfo *)goodShip->staticinfo;
    if (shipstatic->custshipheader.CustShipFire != NULL)
    {
        shipstatic->custshipheader.CustShipFire(goodShip,NULL);
    }
}
void nisObjectShow(nisplaying *NIS, nisevent *event)
{
    SpaceObj *obj = (SpaceObj *)NIS->objectsInMotion[event->shipID].spaceobj;
    ShipRace race = NIS->header->objectPath[event->shipID].race;
    etgeffectstatic *stat;
    Effect *newEffect;

    if (race == NSR_Effect)
    {                                                       //if we are showing an effect, count it as starting up the effect
        //start the effect
        if (!nisSeeking)
        {
            stat = (etgeffectstatic *)NIS->header->objectPath[event->shipID].type;
            univAddObjToRenderList(obj);                    //temp. add this object to render list
            newEffect = etgEffectCreate(stat, obj, NULL, NULL, NULL, 1.0f, EAF_AllButNLips, 0);
            univRemoveObjFromRenderList(obj);               //don't leave this dummy object in the render list
            obj->staticinfo = (StaticInfo *)newEffect;      //store reference to effect in unused member of the spaceobj structure
            bitSet(newEffect->flags, SOF_ForceVisible);     //NIS effects should always be on-screen so force them to be visible
        }
    }
    else
    {                                                       //else any other type of object: add to render list
        if (!univSpaceObjInRenderList(obj))
        {                                                   //if object not in render list
            univAddObjToRenderList(obj);                    //add it to render list
        }
#if NIS_VERBOSE_MODE >= 1
        else
        {
            dbgMessagef("\nnisObjectShow: at time %.2f, object#%d is already visible.", event->time, event->shipID);
        }
#endif
        bitClear(obj->flags, SOF_Hide);                     //make sure it stays in render list
        //bitSet(obj->flags, SOF_ForceVisible);
    }
}
void nisObjectHide(nisplaying *NIS, nisevent *event)
{
    SpaceObj *obj = (SpaceObj *)NIS->objectsInMotion[event->shipID].spaceobj;
    ShipRace race = NIS->header->objectPath[event->shipID].race;

    if (race == NSR_Effect)
    {                                                       //if we are hiding an effect, count it as stopping the effect
        if ((SpaceObj *)obj->staticinfo != NULL)
        {
            //end the effect
            obj = (SpaceObj *)obj->staticinfo;              //this is where the effect pointer is stored.  It better not have deleted itself.
            etgEffectDelete((Effect *)obj);                 //delete the effect
            univRemoveObjFromRenderList(obj);               //remove from render list
            listDeleteNode(&obj->objlink);                  //remove from object list
        }
    }
    else
    {                                                       //else any other type of object: remove from render list
        if (univSpaceObjInRenderList(obj))
        {
            univRemoveObjFromRenderList(obj);
        }
#if NIS_VERBOSE_MODE >= 1
        else
        {
            dbgMessagef("\nnisObjectHide: at time %.2f, object#%d is already hidden.", event->time, event->shipID);
        }
#endif
        bitSet(obj->flags, SOF_Hide);                       //make sure it stays out of render list
        //bitClear(obj->flags, SOF_ForceVisible);
    }
}
void nisInvincible(nisplaying *NIS, nisevent *event)
{
    ((Ship *)NIS->objectsInMotion[event->shipID].spaceobj)->health = REALlyBig;
}
void nisVincible(nisplaying *NIS, nisevent *event)
{
    Ship *ship = (Ship *)NIS->objectsInMotion[event->shipID].spaceobj;
    ship->health = ship->staticinfo->maxhealth;
}
void nisSoundEvent(nisplaying *NIS, nisevent *event)
{
    if (nisSeeking)
    {
        return;
    }
    soundEvent(NIS->objectsInMotion[event->shipID].spaceobj, event->param[0]);
}
void nisSpeechEvent(nisplaying *NIS, nisevent *event)
{
    sdword actor;

    if ((event->param[1] & NIS_ActorMask) == NIS_ActorMask)
    {
        actor = SOUND_EVENT_DEFAULT;
    }
    else
    {
        actor = (event->param[1] & NIS_ActorMask) >> NIS_ActorShiftBits;
    }
    if (nisSeeking)
    {
        return;
    }
    //speechEvent(NIS->objectsInMotion[event->shipID].spaceobj, event->param[0], event->param[1]);
    speechEventQueue(NIS->objectsInMotion[event->shipID].spaceobj,
                     event->param[0], event->param[1] & (~NIS_ActorMask),
                     SOUND_EVENT_DEFAULT, actor,
                     universe.curPlayerIndex, SOUND_EVENT_DEFAULT,
                     (real32)SOUND_EVENT_DEFAULT, SOUND_EVENT_DEFAULT);
}
void nisFleetSpeechEvent(nisplaying *NIS, nisevent *event)
{
    if (nisSeeking)
    {
        return;
    }
    speechEventFleet(event->param[0], event->param[1], universe.curPlayerIndex);
}

void nisAnimaticSpeechEvent(nisplaying *NIS, nisevent *event)
{
    speechEventFleet(event->param[0], event->param[1], universe.curPlayerIndex);
}
void nisSMPTEOn(nisplaying *NIS, nisevent *event)
{
    nisSMPTECounter = (nisSMPTE *)event->param[0];
}
void nisSMPTEOff(nisplaying *NIS, nisevent *event)
{
    nisSMPTECounter = NULL;
}
/*-----------------------------------------------------------------------------
    Name        : nisStaticOnExp
    Description : Exported function to turn static on.  Can be used by other modules.
    Inputs      : newStatic - parameters for static.
    Outputs     :
    Return      :
----------------------------------------------------------------------------*/
void nisStaticOnExp(nisstatic *newStatic)
{
    sdword count;
    real32 minHue, maxHue, minLum, maxLum, minSat, maxSat;
    real32 red, green, blue;
    real32 hue, lum;
    real32 sat;
    udword minAlpha, maxAlpha, alphaUbyte;
    color textureData[NIS_NoiseTextureWidth * NIS_NoiseTextureHeight * sizeof(color)];
    color *texturePointer = textureData;

    //create the static texture if needed
    if (newStatic->bTextured)
    {                                                       //if indeed it is textured
        minHue = max(newStatic->hue - newStatic->hueVariation, 0.0f);
        maxHue = min(newStatic->hue + newStatic->hueVariation, 1.0f);
        minSat = max(newStatic->sat - newStatic->satVariation, 0.0f);
        maxSat = min(newStatic->sat + newStatic->satVariation, 1.0f);
        minLum = max(newStatic->lum - newStatic->lumVariation, 0.0f);
        maxLum = min(newStatic->lum + newStatic->lumVariation, 1.0f);
        minAlpha = colRealToUbyte(max(newStatic->alpha - newStatic->alphaVariation, 0.0f));
        maxAlpha = colRealToUbyte(min(newStatic->alpha + newStatic->alphaVariation, 1.0f));
        for (count = NIS_NoiseTextureWidth * NIS_NoiseTextureHeight; count > 0; count--, texturePointer++)
        {                                                   //fill in the texture bitmap
            hue = frandyrandombetween(RAN_Static, minHue, maxHue);
            sat = frandyrandombetween(RAN_Static, minSat, maxSat);
            lum = frandyrandombetween(RAN_Static, minLum, maxLum);
            alphaUbyte = randyrandombetween(RAN_Static, minAlpha, maxAlpha);
            colHLSToRGB(&red, &green, &blue, hue, lum, sat);
            dbgAssert(red >= 0.0f && red <= 1.0f);
            dbgAssert(green >= 0.0f && green <= 1.0f);
            dbgAssert(blue >= 0.0f && blue <= 1.0f);
            *texturePointer = colRGBA(colRealToUbyte(red), colRealToUbyte(green), colRealToUbyte(blue), alphaUbyte);
        }
        newStatic->texture = trRGBTextureCreate(textureData, NIS_NoiseTextureWidth, NIS_NoiseTextureHeight, newStatic->bAlpha);
    }
    else
    {                                                       //else no texture
        newStatic->texture = TR_InvalidInternalHandle;
    }

    nisStatic[newStatic->index] = newStatic;
}

/*-----------------------------------------------------------------------------
    Name        : nisStaticOffExp
    Description : Exported function for turning off a particular static channel.
    Inputs      : index - what channel to disable
    Outputs     :
    Return      :
----------------------------------------------------------------------------*/
void nisStaticOffExp(sdword index)
{
    dbgAssert(index >= 0 && index < NIS_NumberStatics + NIS_NumberExtraStatics);

    if (nisStatic[index] != NULL)
    {                                                       //if this static is on
        if (nisStatic[index]->texture != TR_InvalidInternalHandle)
        {                                                   //and there even is some static here
            trRGBTextureDelete(nisStatic[index]->texture);
            nisStatic[index]->texture = TR_InvalidInternalHandle;
        }
        nisStatic[index] = NULL;
    }
}
void nisStaticOn(nisplaying *NIS, nisevent *event)
{
    nisstatic *newStatic = (nisstatic *)event->param[0];

    nisStaticOnExp(newStatic);
}
void nisStaticOff(nisplaying *NIS, nisevent *event)
{
    nisStaticOffExp(event->param[0]);
}

void nisHideDerelict(nisplaying *NIS, nisevent *event)
{
    Node *node;
    Derelict *derelict;
    //find a derelict of the specified type
    for (node = universe.DerelictList.head; node != NULL; node = node->next)
    {
        derelict = (Derelict *)listGetStructOfNode(node);
        if (derelict->derelicttype == event->param[0])
        {                                                   //if this is the derelict we're looking for
            if (event->code == NEO_HideDerelict)
            {
                if (derelict->staticinfo->worldRender)
                {                                           //if it's a world-render type derelict
                    univRemoveFromWorldList(derelict);
                }
                else
                {
                    bitSet(derelict->flags, SOF_Hide);
                    if (univSpaceObjInRenderList((SpaceObj *)derelict))
                    {
                        univRemoveObjFromRenderList((SpaceObj *)derelict);
                    }
                }
            }
            else
            {                                               //else we're mot hiding it; we're showing it
                if (derelict->staticinfo->worldRender)
                {                                           //if it's a world-render type of derelict
                    univAddToWorldList(derelict);
                }
                else
                {
                    bitClear(derelict->flags, SOF_Hide);
                }
            }
            break;                                          //only process 1 derelict
        }
    }
}

/*-----------------------------------------------------------------------------
    Name        : nisNewNIS
    Description : Starts a new NIS
    Inputs      :
    Outputs     :
    Return      :
----------------------------------------------------------------------------*/
void nisNewNIS(nisplaying *NIS, nisevent *event)
{
    vector tempPosition;
    matrix tempCoordSys;

    if (nisSeeking)
    {                                                       //if seeking or very close to the end of NIS
        return;
    }
    tempPosition = thisNisPlaying->nisPosition;
    tempCoordSys = thisNisPlaying->nisMatrix;
    nisNewNISStarted = TRUE;
    nisStop(NIS);
    thisNisHeader = nisLoad((char *)event->param[0], (char *)event->param[1]);
    thisNisPlaying = nisStart(thisNisHeader, &tempPosition, &tempCoordSys);
}

void nisAttributes(nisplaying *NIS, nisevent *event)
{
    SpaceObj *obj = (SpaceObj *)NIS->objectsInMotion[event->shipID].spaceobj;
    obj->attributes = (uword)event->param[0];
}

void nisAttackKasSelection(nisplaying *NIS, nisevent *event)
{
    SelectCommand thisShip;                                 //selection of length 1

    thisShip.numShips = 1;                                 //make a temp selection of just this ship
    thisShip.ShipPtr[0] = (Ship *)NIS->objectsInMotion[event->shipID].spaceobj;
    clWrapAttack(&universe.mainCommandLayer,&thisShip,(AttackCommand *)event->param[0]);
}

void nisKeepMovingAtEnd(nisplaying *NIS, nisevent *event)
{
    bitSet(NIS->objectsInMotion[event->shipID].flags, OMF_KeepOnMoving);
}

void nisMinimumLOD(nisplaying *NIS, nisevent *event)
{
    Ship *goodShip;

    goodShip = (Ship *)NIS->objectsInMotion[event->shipID].spaceobj;
    goodShip->staticinfo->staticheader.LOD->nLevels = event->param[0];
}

void nisDamageLevel(nisplaying *NIS, nisevent *event)
{
    Ship *goodShip;
    real32 health = TreatAsReal32(event->param[0]);

    goodShip = (Ship *)NIS->objectsInMotion[event->shipID].spaceobj;
    goodShip->health = health * goodShip->staticinfo->maxhealth;
}

#pragma warning( 4 : 4047)      // turns off "different levels of indirection warning"
void nisRemainAtEnd(nisplaying *NIS, nisevent *event)
{
    bitSet(NIS->objectsInMotion[event->shipID].flags, OMF_RemainAtEnd);
    if (event->param[0])
    {
        char *str = (char *)event->param[0];
        Ship *ship = (Ship *)NIS->objectsInMotion[event->shipID].spaceobj;
        dbgAssert(strlen(str) > 0);
        dbgAssert(ship->objtype == OBJ_ShipType);

        if (singlePlayerGame)
        {
            kasAddShipToTeam(ship,str);
        }

        memFree((void *)event->param[0]);
        event->param[0] = 0;
    }
}
#pragma warning( 2 : 4047)      // turn back on "different levels of indirection warning"

void nisCameraFOV(nisplaying *NIS, nisevent *event)
{
    real32 FOV = TreatAsReal32(event->param[0]);

    nisCamera->fieldofview = FOV;
}
void nisCameraCut(nisplaying *NIS, nisevent *event)
{
    sdword index, j;
    objectmotion *path;
    cameramotion *camPath;
    vector position;
    real32 timeElapsed = TreatAsReal32(event->param[0]);

    dbgAssert(timeElapsed >= 0);
    dbgAssert(timeElapsed < NIS->header->length);

    if (nisSeeking)
    {
        return;
    }
    position = NIS->nisPosition;                            //get position from NIS matrix
    //advance all the object motion paths
    for (index = 0; index < NIS->header->nObjectPaths; index++)
    {
        path = &NIS->objectsInMotion[index];                //get pointer to object motion path
        if (path->curve[0] != NULL)
        {                                                   //if NULL motion path or NULL object
            for (j = 0; j < 6; j++)
            {                                               //for each motion channel
                bsCurveUpdate(path->curve[j], timeElapsed);    //update to a good point
            }
        }
    }
    //advance all the camera motion paths
    for (index = 0; index < NIS->header->nCameraPaths; index++)
    {
        camPath = &NIS->camerasInMotion[index];     //get pointer to object motion path
        if (camPath->curve[0] == NULL)
        {                                           //if NULL camera motion path
            continue;                               //leave this object where it was
        }
        for (j = 0; j < 6; j++)
        {                                           //for each motion channel
            bsCurveUpdate(camPath->curve[j], timeElapsed);//update to a good point
        }
    }
    //advance the official time of the event
    NIS->timeElapsed += timeElapsed;
}
void nisTextCard(nisplaying *NIS, nisevent *event)
{
    if (nisSeeking)
    {
        return;
    }
    dbgAssert(nisTextCardIndex < NIS_NumberTextCards);

    nisTextCardList[nisTextCardIndex] = *((nistextcard *)event->param[0]);
    nisTextCardList[nisTextCardIndex].creationTime = NIS->timeElapsed;
    nisTextCardList[nisTextCardIndex].NIS = NIS;
    nisTextCardIndex++;
}
void nisTextScroll(nisplaying *NIS, nisevent *event)
{
    if (nisSeeking)
    {
        return;
    }
    nisTextScrollDistance = TreatAsReal32(event->param[0]);
    nisTextScrollStart = NIS->timeElapsed;
    nisTextScrollEnd = nisTextScrollStart + TreatAsReal32(event->param[1]);
}
void nisScissorOut(nisplaying *NIS, nisevent *event)
{
    soundEvent(NULL, UI_Unletterbox);
    nisScissorFadeOut = TreatAsReal32(event->param[0]);
    nisScissorFadeTime = 0.0f;
}
void nisFocusAtEnd(nisplaying *NIS, nisevent *event)
{
    bitSet(NIS->objectsInMotion[event->shipID].flags, OMF_FocusAtEnd);
}

/*-----------------------------------------------------------------------------
    Name        : nisFocusMaxRadius
    Description : Find the radius of a sphere that would completely encompass
                    the specified selection if centred about centre.
    Inputs      : centre - centre of sphere to compute radius of
                  selection - selection of ships to fit in sphere
    Outputs     :
    Return      : radius of sphere
----------------------------------------------------------------------------*/
real32 nisFocusMaxRadius(vector *centre, MaxSelection *selection)
{
    sdword index;
    vector shipCentre, distance;
    Ship *ship;
    real32 radius, maxRadius = 0.0f;

    for (index = 0; index < selection->numShips; index++)
    {
        ship = selection->ShipPtr[index];
        ccGetShipCollCenter(ship, &shipCentre, &radius);    //get location of ship
        vecSub(distance, *centre, shipCentre);              //get distance from centre
        radius = vecMagnitudeSquared(distance) + radius;    //get d^2
        if (radius > maxRadius)
        {                                                   //if new record
            maxRadius = radius;
        }
    }
    return(fsqrt(maxRadius));                               //return max distance
}

/*-----------------------------------------------------------------------------
    Name        : nisFocusPointCompute
    Description : Compute the focal point of a list of ships
    Inputs      : selection - selection to scan
    Outputs     : centre - computed centre point
    Return      :
----------------------------------------------------------------------------*/
void nisFocusPointCompute(vector *centre, MaxSelection *selection)
{
    sdword index;
    Ship *ship;
    real32 minx,maxx,miny,maxy,minz,maxz, radius;

    dbgAssert(selection->numShips > 0);

    ship = selection->ShipPtr[0];

    ccGetShipCollCenter(ship, centre, &radius);

    minx = maxx = centre->x;
    miny = maxy = centre->y;
    minz = maxz = centre->z;

    for (index = 1; index < selection->numShips; index++)
    {
        ship = selection->ShipPtr[index];
        ccGetShipCollCenter(ship, centre, &radius);

        if (centre->x < minx) minx = centre->x;
        else if (centre->x > maxx) maxx = centre->x;

        if (centre->y < miny) miny = centre->y;
        else if (centre->y > maxy) maxy = centre->y;

        if (centre->z < minz) minz = centre->z;
        else if (centre->z > maxz) maxz = centre->z;
    }

    centre->x = (minx + maxx) * 0.5f;
    centre->y = (miny + maxy) * 0.5f;
    centre->z = (minz + maxz) * 0.5f;
}

/*-----------------------------------------------------------------------------
    Name        : nisFocus
    Description : NIS-event callback to focus the camera on the specified ships
    Inputs      :
    Outputs     :
    Return      :
----------------------------------------------------------------------------*/
void nisFocus(nisplaying *NIS, nisevent *event)
{
    sdword index;
    MaxSelection tempselected;
    vector focusPoint, distance, lookDirection;

    tempselected.numShips = 0;
    for (index = 0; index < NIS->nObjects; index++)
    {                                                       //find ships to focus on
        if (bitTest(NIS->objectsInMotion[index].flags, OMF_FocusAtEnd))
        {
            tempselected.ShipPtr[tempselected.numShips] = (ShipPtr)NIS->objectsInMotion[index].spaceobj;
            tempselected.numShips++;
        }
    }

    if (tempselected.numShips > 0)
    {                                                       //if there were ships to focus on
        //find the distance to the focus point.
        nisCameraPrepareForCopy(nisCamera);
        nisFocusPointCompute(&focusPoint, &tempselected);
        if (bPerfectFocusComputed)
        {
            nisCamera->lookatpoint = focusPoint;
        }
        else
        {
            vecSub(distance, focusPoint, nisCamera->eyeposition);
            vecSub(lookDirection, nisCamera->lookatpoint, nisCamera->eyeposition);
            nisCamera->distance = fsqrt(vecMagnitudeSquared(distance));
            vecMultiplyByScalar(lookDirection, nisCamera->distance);
            vecAdd(nisCamera->lookatpoint, nisCamera->eyeposition, lookDirection);
        }
        //set the azimuth and declination properly
        nisCamera->oldlookatpoint = nisCamera->lookatpoint;

        //now it's safe to copy the camera
        ccCopyCamera(&universe.mainCameraCommand,nisCamera);    //focus the camera to wherever it is
        ccFocus(&universe.mainCameraCommand,(FocusCommand *)&tempselected);
    }
    else
    {                                                       //no ships to focus on, focus on the previous scene
        nisCameraPrepareForCopy(nisCamera);
        nisCamera->oldlookatpoint = nisCamera->lookatpoint;
        ccCopyCamera(&universe.mainCameraCommand,nisCamera);    //focus the camera from wherever it is
//        ccCancelFocus(&(universe.mainCameraCommand));
        ccFocusGeneral(&universe.mainCameraCommand, &universe.mainCameraCommand.currentCameraStack->focus, FALSE);
    }
    bCameraFocussed = TRUE;
    nisCaptureCamera = FALSE;
    nisCamera = NULL;
    nisCameraCutTime = 0.0f;                                //in case player hit (escape)
    nisCameraCutStage2 = -1.0f;
}
void nisMusicStart(nisplaying *NIS, nisevent *event)
{
    if (nisMusicPlaying)
    {                                                       //if any music is playing
        soundEventStopMusic(0.0f);                          //stop it
        nisMusicPlaying = FALSE;
    }
#if NIS_VERBOSE_LEVEL >= 1
    dbgMessagef("\nPlaying music track %d.", event->param[0]);
#endif
    soundEventPlayMusic(event->param[0]);
    nisMusicPlaying = TRUE;
}
void nisMusicStop(nisplaying *NIS, nisevent *event)
{
#if NIS_VERBOSE_LEVEL >= 1
    dbgMessagef("\nStopping music over the next %.2f seconds.", TreatAsReal32(event->param[0]));
#endif
    soundEventStopMusic(TreatAsReal32(event->param[0]));
    nisMusicPlaying = FALSE;
}
void nisBlackFadeSet(nisplaying *NIS, nisevent *event)
{
#if NIS_VERBOSE_LEVEL >= 1
    dbgMessagef("\nSetting fade level to %.2f", TreatAsReal32(event->param[0]));
#endif
    nisBlackFade = nisBlackFadeDest = TreatAsReal32(event->param[0]);
}
void nisBlackFadeTo(nisplaying *NIS, nisevent *event)
{
#if NIS_VERBOSE_LEVEL >= 1
    dbgMessagef("\nFading to level %.2f over %.2f seconds", TreatAsReal32(event->param[0]), TreatAsReal32(event->param[1]));
#endif
    nisBlackFadeDest = TreatAsReal32(event->param[0]);
    nisBlackFadeRate = (TreatAsReal32(event->param[0]) - nisBlackFade) / TreatAsReal32(event->param[1]);
}
void nisColorScheme(nisplaying *NIS, nisevent *event)
{
    SpaceObjRotImp *goodShip = NIS->objectsInMotion[event->shipID].spaceobj;

    switch (goodShip->objtype)
    {
        case OBJ_ShipType:
            ((Ship *)goodShip)->colorScheme = event->param[0];
            break;
        case OBJ_MissileType:
            ((Missile *)goodShip)->colorScheme = event->param[0];
            break;
        case OBJ_DerelictType:
            ((Derelict *)goodShip)->colorScheme = event->param[0];
            break;
        default:
            dbgAssert(FALSE);
    }
}
void nisMeshAnimationStart(nisplaying *NIS, nisevent *event)
{
    Ship *goodShip = (Ship *)NIS->objectsInMotion[event->shipID].spaceobj;
    sdword animIndex;
    madanim *anim;

    dbgAssert(goodShip->madBindings != NULL);
    anim = goodShip->madBindings;
    animIndex = madAnimIndexFindByName(anim->header, (char *)event->param[0]);
    if (anim->nCurrentAnim != -1)
    {                                                       //if there is an animation playing
        //!!! store current animation info somewhere and delete it when we're done
        madAnimationStop(goodShip);                         //stop any animations currently playing
    }
    madAnimationStart(goodShip, animIndex);                 //start the animation playing
}
void nisMeshAnimationStop(nisplaying *NIS, nisevent *event)
{
    Ship *goodShip = (Ship *)NIS->objectsInMotion[event->shipID].spaceobj;

    dbgAssert(goodShip->madBindings != NULL);
    if (goodShip->madBindings->nCurrentAnim != -1)
    {                                                       //if there is an animation playing
        madAnimationStop(goodShip);
    }
}
void nisMeshAnimationPause(nisplaying *NIS, nisevent *event)
{
    Ship *goodShip = (Ship *)NIS->objectsInMotion[event->shipID].spaceobj;

    dbgAssert(goodShip->madBindings != NULL);
    if (goodShip->madBindings->nCurrentAnim != -1)
    {                                                       //if there is an animation playing
        madAnimationPause(goodShip, goodShip->madBindings->bPaused ^ TRUE);
    }
}
void nisMeshAnimationSeek(nisplaying *NIS, nisevent *event)
{
    Ship *goodShip = (Ship *)NIS->objectsInMotion[event->shipID].spaceobj;
    real32 seekTime;

    dbgAssert(goodShip->madBindings != NULL);
    if (goodShip->madBindings->nCurrentAnim != -1)
    {                                                       //if there is an animation playing
//        goodShip->madBindings->time = TreatAsReal32(event->param[0]);
        seekTime = TreatAsReal32(event->param[0]);
        if (seekTime > goodShip->madBindings->time)
        {
            madAnimationUpdate(goodShip, seekTime - goodShip->madBindings->time);
        }
    }
}
/*-----------------------------------------------------------------------------
    Name        : nisBlendCameraEndPoint
    Description : NIS-event function that computes the focus point of all this
                    ships that will be focussed on at the end of the NIS and
                    plugs that point into the last keyframe of the NIS camera's
                    motion path.
    Inputs      :
    Outputs     :
    Return      :
----------------------------------------------------------------------------*/
void nisBlendCameraEndPoint(nisplaying *NIS, nisevent *event)
{
    sdword index;
    MaxSelection tempselected;
    vector position, difference;
    vector presumedFinalPosition, presumedFinalRotation, presumedFinalLook;
    vector xyz, xyzTemp;
    vector Vfocus;
    matrix tempCoordsys, coordsys;
    real32 **path;
    real32 currentTimeElapsed;

    tempselected.numShips = 0;
    for (index = 0; index < NIS->nObjects; index++)
    {                                                       //find ships to focus on
        if (bitTest(NIS->objectsInMotion[index].flags, OMF_FocusAtEnd))
        {
            tempselected.ShipPtr[tempselected.numShips] = (ShipPtr)NIS->objectsInMotion[index].spaceobj;
            tempselected.numShips++;
        }
    }
    //compute the centre of that selection
    nisFocusPointCompute(&position, &tempselected);
    //now plug that postion into the last and second-from-last keyframes of this NIS camera's motion path
    index = NIS->header->cameraPath[0].nSamples;
    path = NIS->header->cameraPath[0].curve;
    //camera position: -Z, X, Y (UZI mode 33)
    xyzTemp.x = -path[2][index - 1];
    xyzTemp.y = path[0][index - 1];
    xyzTemp.z = path[1][index - 1];

    matMultiplyMatByVec(&xyz, &NIS->nisMatrix, &xyzTemp);   //multiply NIS world matrix

    presumedFinalPosition.x = NIS->nisPosition.x + xyz.x;
    presumedFinalPosition.y = NIS->nisPosition.y + xyz.y;
    presumedFinalPosition.z = NIS->nisPosition.z + xyz.z;

    //camera rotation: X, Y, Z (mode 0)
    presumedFinalRotation.x = path[3][index - 1];
    presumedFinalRotation.y = path[4][index - 1];
    presumedFinalRotation.z = path[5][index - 1];

    nisShipEulerToMatrix(&tempCoordsys, &presumedFinalRotation);
    matMultiplyMatByMat(&coordsys, &NIS->nisMatrix, &tempCoordsys);//multiply NIS world matrix
    matGetVectFromMatrixCol2(presumedFinalLook, coordsys);

    //focus point and camera.eye-focus point defines a plane
    vecSub(Vfocus, presumedFinalPosition, position);
    //camera.eye and camera.eye-camera.look defines a ray
    //compute the intersection of these.
    vecNormalize(&Vfocus);       //!!! needed?
    vecLineIntersectWithPlane(&xyz, &position, &Vfocus, &presumedFinalPosition, &presumedFinalLook);
    vecSub(difference, xyz, position);
    matCopyAndTranspose(&NIS->nisMatrix, &tempCoordsys);    //get the transpose of the NIS matrix
    matMultiplyMatByVec(&xyz, &NIS->nisMatrix, &difference);//get difference in 'NIS space'

    //game back to NIS: y, z, -x (inverse of UZI mode 33)
    path[0][index - 1] -= xyz.y;                            //adjust the last keyframe
    path[1][index - 1] -= xyz.z;
    path[2][index - 1] += xyz.x;
    path[0][index - 2] -= xyz.y;                            //adjust the second-to-last keyframe
    path[1][index - 2] -= xyz.z;
    path[2][index - 2] += xyz.x;
    path[0][index - 3] -= xyz.y;                            //adjust the third-to-last keyframe
    path[1][index - 3] -= xyz.z;
    path[2][index - 3] += xyz.x;

    bPerfectFocusComputed = TRUE;

    //if we are seeking, we should update the camera at this point because the keyframes have changed
    if (nisSeeking)
    {
        currentTimeElapsed = NIS->camerasInMotion[0].curve[0]->timeElapsed;
        for (index = 0; index < 6; index++)
        {                                               //for each motion channel
            bsCurveRestart(NIS->camerasInMotion[0].curve[index]);      //restart the curve
        }
        nisCamPathUpdate(NIS, &NIS->camerasInMotion[0], currentTimeElapsed);//advance it just a tiny little bit
    }
}

/*-----------------------------------------------------------------------------
    Name        : nisVolumeSet
    Description : NIS-event callback for setting the volume of either music, speech or SFX
    Inputs      :
    Outputs     :
    Return      :
----------------------------------------------------------------------------*/
void nisVolumeSet(nisplaying *NIS, nisevent *event)
{
    switch (event->code)
    {
        case NEO_MusicVolume:
#if NIS_VERBOSE_LEVEL > 2
            dbgMessagef("\nSetting Music volume to %.2f", TreatAsReal32(event->param[0]));
#endif
            soundEventMusicVol(TreatAsReal32(event->param[0]));
            break;
        case NEO_SpeechVolume:
#if NIS_VERBOSE_LEVEL > 2
            dbgMessagef("\nSetting Speech volume to %.2f", TreatAsReal32(event->param[0]));
#endif
            soundEventSpeechVol(TreatAsReal32(event->param[0]));
            break;
        case NEO_SFXVolume:
#if NIS_VERBOSE_LEVEL > 2
            dbgMessagef("\nSetting SFX volume to %.2f", TreatAsReal32(event->param[0]));
#endif
            soundEventSFXVol(TreatAsReal32(event->param[0]));
            break;
    }
}

/*-----------------------------------------------------------------------------
    Name        : nisTrailMove
    Description : NIS-event callback for moving a ship's trail by the difference
                    of two upcoming adjacent keyframes
    Inputs      :
    Outputs     :
    Return      :
----------------------------------------------------------------------------*/
#define NIS_AdjacentFrames      (1.0 / 15.0)
#define NIS_NotAnyTimeSoon      1.5f
void nisTrailMove(nisplaying *NIS, nisevent *event)
{
    Ship *ship;
    sdword pointIndex, index;
    real32 difference;
    splinecurve **curve;
    vector xyzTemp, moveAmount;

/*
    if (nisSeeking)
    {
        return;
    }
*/
    ship = (Ship *)NIS->objectsInMotion[event->shipID].spaceobj;
    dbgAssert(ship->objtype == OBJ_ShipType);
    curve = NIS->objectsInMotion[event->shipID].curve;
    pointIndex = max(curve[0]->currentPoint + 1, 0);
    for (; pointIndex < curve[0]->nPoints - 1; pointIndex++)
    {                                                       //look for adjacent frames
        if (curve[0]->times[pointIndex] - NIS->timeElapsed >= NIS_NotAnyTimeSoon)
        {                                                   //if no adjacent frames coming up
            break;
        }
        difference = curve[0]->times[pointIndex] - curve[0]->times[pointIndex - 1];
        if (difference > 0.0f && difference < NIS_AdjacentFrames)
        {                                                   //if these 2 frames are quite close together
            xyzTemp.x = curve[0]->points[pointIndex - 1] - curve[0]->points[pointIndex];
            xyzTemp.y = curve[1]->points[pointIndex - 1] - curve[1]->points[pointIndex];
            xyzTemp.z = curve[2]->points[pointIndex - 1] - curve[2]->points[pointIndex];
            //object position:-Z, X, Y (UZI position mode 33)
            moveAmount.x = -xyzTemp.z;
            moveAmount.y = xyzTemp.x;
            moveAmount.z = xyzTemp.y;
            for (index = 0; index < MAX_NUM_TRAILS; index++)
            {                                               //for each trail in the ship
                if (ship->trail[index] != NULL)
                {
                    trailMove(ship->trail[index], &moveAmount);//move it a bit
                }
            }
            return;
        }
    }
#if NIS_ERROR_CHECKING
    dbgWarningf(DBG_Loc, "No nearby consecutive frames at %.2f.", NIS->timeElapsed);
#endif
}

/*-----------------------------------------------------------------------------
    Name        : nisTrailZero
    Description : NIS-event callback for zeroing the length of a ship's engine trail(s)
    Inputs      :
    Outputs     :
    Return      :
----------------------------------------------------------------------------*/
void nisTrailZero(nisplaying *NIS, nisevent *event)
{
    Ship *ship;
    sdword index;

    ship = (Ship *)NIS->objectsInMotion[event->shipID].spaceobj;
    for (index = 0; index < MAX_NUM_TRAILS; index++)
    {                                                       //for every trail in the ship
        if (ship->trail[index] != NULL)
        {
            trailZeroLength(ship->trail[index]);            //zero it out
        }
    }
}

/*-----------------------------------------------------------------------------
    Name        : nisUniversePauseToggle
    Description : NIS-event callback for pausing the universe
    Inputs      :
    Outputs     :
    Return      :
----------------------------------------------------------------------------*/
void nisUniversePauseToggle(nisplaying *NIS, nisevent *event)
{
    nisUniversePause ^= TRUE;
#if NIS_VERBOSE_LEVEL >= 1
    dbgMessagef("\nNIS has %sPAUSED the universe.", nisUniversePause ? "" : "UN");
#endif
}

/*-----------------------------------------------------------------------------
    Name        : nisNewEnvironment
    Description : Set the background and lighting to something new.
    Inputs      :
    Outputs     :
    Return      :
----------------------------------------------------------------------------*/
void nisNewEnvironment(nisplaying *NIS, nisevent *event)
{
    nisenvironment *env = (nisenvironment *)event->param[0];
    char path[_MAX_PATH];

    if (nisSeeking)
    {
        return;
    }
    if (nisPreviousBackground == NULL)
    {
        nisPreviousBackground = memStringDupeNV(btgLastBackground);
    }
    if (nisPreviousLighting == NULL)
    {
        nisPreviousLighting = memStringDupeNV(lightCurrentLighting);
    }
    sprintf(path, "BTG\\%s.btg", env->name);                //load the background
    nisPreviousTheta = btgGetTheta();
    nisPreviousPhi = btgGetPhi();
    btgSetTheta(env->theta);
    btgSetPhi(env->phi);
    btgLoad(path);

    sprintf(path,"hsf\\%s.hsf",env->name);
    lightParseHSF(path);                               //load the lighting
}

/*-----------------------------------------------------------------------------
    Name        : nisUniverseHideToggle
    Description : NIS-event callback for hiding all non-NIS ships in the universe.
    Inputs      :
    Outputs     :
    Return      :
----------------------------------------------------------------------------*/
void nisUniverseHideToggle(nisplaying *NIS, nisevent *event)
{
    nisUniverseHidden ^= TRUE;
#if NIS_VERBOSE_LEVEL >= 1
    dbgMessagef("\nNIS has %sHIDDEN the universe.", nisUniverseHidden ? "" : "UN");
#endif
    if (nisUniverseHidden)
    {                                                       //if hiding everything
        univHideJustAboutEverything();
    }
    else
    {                                                       //else everything being unhidden
        univUnhideJustAboutEverything();                    //show everything previously hidden
        univUpdateRenderList();
    }
}

/*-----------------------------------------------------------------------------
    Name        : nisNewEvent
    Description : Allocate and return a new event.
    Inputs      : opcode - opcode for the new event
    Outputs     : allocates new event, sets the current time and object
    Return      : newly allocated event.
----------------------------------------------------------------------------*/
nisevent *nisNewEvent(nisopcode opcode)
{
    dbgAssert(nisEventIndex >= 0);
    nisEventIndex++;
    nisEventList = memRealloc(nisEventList, sizeof(nisevent) * nisEventIndex, "nisEventBuffer", 0);
    dbgAssert(nisCurrentTime != REALlyBig);
    dbgAssert(nisCurrentObject != -1);
    dbgAssert(opcode >= 0 && opcode < NEO_LastNEO);
    nisEventList[nisEventIndex - 1].time = nisCurrentTime;
    nisEventList[nisEventIndex - 1].shipID = nisCurrentObject;
    nisEventList[nisEventIndex - 1].code = opcode;
    nisEventList[nisEventIndex - 1].initialID = nisEventIndex;
    return(&nisEventList[nisEventIndex - 1]);
}

nisevent* nisNewAnimaticEvent(nisopcode opcode)
{
    dbgAssert(nisEventIndex >= 0);
    nisEventIndex++;
    nisEventList = memRealloc(nisEventList, sizeof(nisevent) * nisEventIndex, "nisEventBuffer", 0);
    dbgAssert(nisCurrentTime != REALlyBig);
    dbgAssert(opcode >= 0 && opcode < NEO_LastNEO);
    nisEventList[nisEventIndex - 1].time = nisCurrentTime;
    nisEventList[nisEventIndex - 1].shipID = nisCurrentObject;
    nisEventList[nisEventIndex - 1].code = opcode;
    nisEventList[nisEventIndex - 1].initialID = nisEventIndex;
    return(&nisEventList[nisEventIndex - 1]);
}

/*-----------------------------------------------------------------------------
    Name        : nisNewEventNoObject
    Description : Allocate and return a new event.
    Inputs      : opcode - opcode for the new event
    Outputs     : allocates new event, sets the current time and object
    Return      : newly allocated event.
----------------------------------------------------------------------------*/
nisevent *nisNewEventNoObject(nisopcode opcode)
{
    dbgAssert(nisEventIndex >= 0);
    nisEventIndex++;
    nisEventList = memRealloc(nisEventList, sizeof(nisevent) * nisEventIndex, "nisEventBuffer", 0);
    dbgAssert(nisCurrentTime != REALlyBig);
    dbgAssert(opcode >= 0 && opcode < NEO_LastNEO);
    nisEventList[nisEventIndex - 1].time = nisCurrentTime;
    nisEventList[nisEventIndex - 1].shipID = 0;
    nisEventList[nisEventIndex - 1].code = opcode;
    nisEventList[nisEventIndex - 1].initialID = nisEventIndex;
    return(&nisEventList[nisEventIndex - 1]);
}

/*-----------------------------------------------------------------------------
    Name        : nisDeathDamageSet
    Description : Script parsing callback
    Inputs      :
    Outputs     :
    Return      :
----------------------------------------------------------------------------*/
void nisDeathDamageSet(char *directory,char *field,void *dataToFillIn)
{
    nisevent *event = nisNewEvent(NEO_DeathDamage);
    event->param[0] = EDT_AccumDamage;
}
void nisDeathProjectileSet(char *directory,char *field,void *dataToFillIn)
{
    nisevent *event = nisNewEvent(NEO_DeathProjectile);
    event->param[0] = EDT_ProjectileHit;
}
void nisDeathBeamSet(char *directory,char *field,void *dataToFillIn)
{
    nisevent *event = nisNewEvent(NEO_DeathBeam);
    event->param[0] = EDT_BeamHit;
}
void nisAttackSet(char *directory,char *field,void *dataToFillIn)
{
    nisevent *event = nisNewEvent(NEO_Attack);
    sdword enemy = nisFindObjectByName(nisCurrentHeader, field);
    event->param[0] = enemy;
}
void nisHaltAttackSet(char *directory,char *field,void *dataToFillIn)
{
    nisevent *event = nisNewEvent(NEO_HaltAttack);
}
void nisFireSet(char *directory,char *field,void *dataToFillIn)
{
    nisevent *event = nisNewEvent(NEO_Fire);
}
//set the current object for subsequent events
void nisCurrentObjectSet(char *directory,char *field,void *dataToFillIn)
{
    nisCurrentObject = nisFindObjectByName(nisCurrentHeader, field);
}
//make no object the current object
void nisCurrentObjectClear(char *directory,char *field,void *dataToFillIn)
{
    nisCurrentObject = -1;
}
//sets the time for sebsequent events
void nisCurrentTimeSet(char *directory,char *field,void *dataToFillIn)
{
    sdword nScanned;
    _strupr(field);
    if (strstr(field, "FRAME") == field)
    {
        nScanned = sscanf(field, "FRAME %f", &nisCurrentTime);
#if NIS_ERROR_CHECKING
        dbgAssert(nScanned == 1);
        if ((nisCurrentTime < 0.0f) || (nisCurrentTime / NIS_FrameRate >= nisCurrentHeader->length))
        {
            dbgFatalf(DBG_Loc, "tried to set time to frame %.0f which is outside nis of length %.0f frames.", nisCurrentTime, nisCurrentHeader->length * NIS_FrameRate);
        }
#endif
        nisCurrentTime /= NIS_FrameRate;                    //convert from frames to seconds
    }
    else
    {
        nScanned = sscanf(field, "%f", &nisCurrentTime);
#if NIS_ERROR_CHECKING
        dbgAssert(nScanned == 1);
        if ((nisCurrentTime < 0.0f) || (nisCurrentTime >= nisCurrentHeader->length))
        {
            dbgFatalf(DBG_Loc, "tried to set time to time %.0f which is outside nis of length %.0f seconds.", nisCurrentTime, nisCurrentHeader->length);
        }
#endif
    }
}
//show an object
void nisCurrentObjectShow(char *directory,char *field,void *dataToFillIn)
{
    nisNewEvent(NEO_Show);
}
//hide an object
void nisCurrentObjectHide(char *directory,char *field,void *dataToFillIn)
{
    nisNewEvent(NEO_Hide);
}
void nisInvincibleSet(char *directory,char *field,void *dataToFillIn)
{
    nisNewEvent(NEO_Invincible);
}
void nisVincibleSet(char *directory,char *field,void *dataToFillIn)
{
    nisNewEvent(NEO_Vincible);
}
void nisSoundEventSet(char *directory,char *field,void *dataToFillIn)
{
    nisevent *event = nisNewEvent(NEO_SoundEvent);
    event->param[0] = event->param[1] = 0;
    sscanf(field, "%d", &event->param[0]);
}
/*-----------------------------------------------------------------------------
    Name        : nisSeparateExpression
    Description : Separate an expression (as contained in parenthesis) and return a pointer to the next item in a string
    Inputs      : in - string that may contain an expression in parenthesis
    Outputs     : out - separated expression
    Return      : pointer to string as it appears after the expression.
----------------------------------------------------------------------------*/
char *nisSeparateExpression(char *in, char *out)
{
    sdword parenNumber = 0;
    static char *delimiters = ", \t";

    while (*in)
    {
        *out = 0;
        *(out + 1) = 0;
        if (*in == '(')
        {                                                   //opening parenthesis
            parenNumber++;
        }
        else if (*in == ')')
        {
            parenNumber--;
            if (parenNumber == 0)
            {                                               //end of expression due to closing parenthesis
                *out = *in;
                dbgAssert(strchr(delimiters, *(in + 1)));
                return(in + 1);
            }
        }
        else if (strchr(delimiters, *in) && parenNumber == 0)
        {
            return(in);
        }
        *out = *in;
        out++;
        in++;
    }
    return(in);
}
void nisSpeechEventSet(char *directory,char *field,void *dataToFillIn)
{
    char *actorString;
    char *nextString;
    char eventNumberString[256];
    double evaluatedNumber;
    ERR_TYPE exprError;
    nisevent *event = nisNewEvent(NEO_SpeechEvent);
    sdword actor = -1;

    event->param[0] = event->param[1] = 0;
    nextString = nisSeparateExpression(field, eventNumberString);
    evaluatedNumber = evalEvaluate(eventNumberString, &exprError);
#if NIS_ERROR_CHECKING
    if (exprError != ALL_OK)
    {
        dbgFatalf(DBG_Loc, "%s in expression '%s'", evalErrorString(exprError), field);
    }
#endif
    event->param[0] = (udword)(sdword)evaluatedNumber;
    if (*nextString)
    {
        sscanf(nextString + 1, "%d", &event->param[1]);
    }
    strupr(field);
    if ((actorString = strstr(field, "ACTOR")) != NULL)
    {
        sscanf(actorString + strlen("ACTOR "), "%d", &actor);
        dbgAssert(actor > 0 && actor <= 3);
        event->param[1] |= actor << NIS_ActorShiftBits;
    }
    else
    {
        event->param[1] |= NIS_ActorMask;
    }
}
void nisFleetSpeechEventSet(char *directory,char *field,void *dataToFillIn)
{
    nisevent *event = nisNewEvent(NEO_FleetSpeechEvent);
    char *nextString;
    char eventNumberString[256];
    double evaluatedNumber;
    ERR_TYPE exprError;

    event->param[0] = event->param[1] = 0;
    nextString = nisSeparateExpression(field, eventNumberString);
    evaluatedNumber = evalEvaluate(eventNumberString, &exprError);
#if NIS_ERROR_CHECKING
    if (exprError != ALL_OK)
    {
        dbgFatalf(DBG_Loc, "%s in expression '%s'", evalErrorString(exprError), field);
    }
#endif
    event->param[0] = (udword)(sdword)evaluatedNumber;
    if (*nextString)
    {
        sscanf(nextString + 1, "%d", &event->param[1]);
    }
}
void nisAnimaticSpeechEventSet(char* directory, char* field, void* dataToFillIn)
{
    nisevent* event = nisNewAnimaticEvent(NEO_AnimaticSpeechEvent);
    char *nextString;
    char eventNumberString[256];
    double evaluatedNumber;
    ERR_TYPE exprError;

    event->param[0] = event->param[1] = 0;
    nextString = nisSeparateExpression(field, eventNumberString);
    evaluatedNumber = evalEvaluate(eventNumberString, &exprError);
#if NIS_ERROR_CHECKING
    if (exprError != ALL_OK)
    {
        dbgFatalf(DBG_Loc, "%s in expression '%s'", evalErrorString(exprError), field);
    }
#endif
    event->param[0] = (udword)(sdword)evaluatedNumber;
    if (*nextString)
    {
        sscanf(nextString + 1, "%d", &event->param[1]);
    }
}
#pragma warning( 4 : 4047)      // turns off "different levels of indirection warning"
void nisRemainAtEndSet(char *directory,char *field,void *dataToFillIn)
{
    char optionalTeamName[50];
    nisevent *event;

    optionalTeamName[0] = 0;
    sscanf(field, "%s", optionalTeamName);

    event = nisNewEvent(NEO_RemainAtEnd);
    if (strlen(optionalTeamName) > 0)
    {
        event->param[0] = (udword)memStringDupe(optionalTeamName);
    }
    else
    {
        event->param[0] = 0;
    }
}
#pragma warning( 2 : 4047)      // turn back on "different levels of indirection warning"

void nisCameraFOVSet(char *directory,char *field,void *dataToFillIn)
{
    nisevent *event = nisNewEvent(NEO_CameraFOV);
    sscanf(field, "%f", &event->param[0]);
}
void nisRaceSwap(char *directory,char *field,void *dataToFillIn)
{
    sdword index;

    if (universe.curPlayerPtr->race == R1)
    {                                                       //player is already race 1
        return;
    }
    for (index = 0; index < nisCurrentHeader->nObjectPaths; index++)
    {                                                       //swap race of all obj paths
        if (nisCurrentHeader->objectPath[index].race == R1)
        {
            nisCurrentHeader->objectPath[index].race = R2;
        }
        else if(nisCurrentHeader->objectPath[index].race == R2)
        {
            nisCurrentHeader->objectPath[index].race = R1;
        }
    }
    bitSet(nisCurrentHeader->flags, NHF_RacesSwapped);      //and remember that we swapped it
}
void nisCameraCutSet(char *directory,char *field,void *dataToFillIn)
{
    nisevent *event = nisNewEventNoObject(NEO_CameraCut);
    sdword nScanned;
    real32 value;
    _strupr(field);
    if (strstr(field, "FRAME"))
    {
        nScanned = sscanf(field, "%f FRAME", &value);
#if NIS_ERROR_CHECKING
        dbgAssert(nScanned == 1);
#endif
        value /= NIS_FrameRate;                   //convert from frames to seconds
    }
    else
    {
        nScanned = sscanf(field, "%f", &value);
#if NIS_ERROR_CHECKING
        dbgAssert(nScanned == 1);
#endif
    }
    event->param[0] = TreatAsUdword(value);
}
void nisBlackScreenSet(char *directory, char *field, void *dataToFillIn)
{
    nisevent *event = nisNewEventNoObject(NEO_BlackScreenStart);
}
void nisEndBlackScreenSet(char *directory, char *field, void *dataToFillIn)
{
    nisevent *event = nisNewEventNoObject(NEO_BlackScreenEnd);
}
void nisTextFontSet(char *directory, char *field, void *dataToFillIn)
{
    nisCurrentTextCardFont = frFontRegister(field);
}
void nisTextColorSet(char *directory, char *field, void *dataToFillIn)
{
    sdword nScanned, red, green, blue;

    nScanned = sscanf(field, "%d,%d,%d", &red, &green, &blue);
#if NIS_ERROR_CHECKING
    dbgAssert(nScanned == 3);
    dbgAssert(red >= 0 && red < 256 && green >= 0 && green < 256 && blue >= 0 && blue < 256);
#endif
    nisCurrentTextCardColor = colRGB(red, green, blue);
}
void nisTextPositionSet(char *directory, char *field, void *dataToFillIn)
{
    sdword nScanned, x, y;

    nScanned = sscanf(field, "%d,%d", &x, &y);
#if NIS_ERROR_CHECKING
    dbgAssert(nScanned == 2);
    dbgAssert(x >= 0 && x < MAIN_WindowWidth);
    dbgAssert(y >= 0 && y < MAIN_WindowHeight * 10);
#endif
    nisCurrentTextCardX = x;
    nisCurrentTextCardY = y;
}
void nisTextScrollSet(char *directory, char *field, void *dataToFillIn)
{
    nisevent *event = nisNewEventNoObject(NEO_TextScroll);
    sdword nScanned;
    real32 duration, distance;

    _strupr(field);
    if (strstr(field, "FRAME"))
    {
        nScanned = sscanf(field, "%f,%f FRAME", &distance, &duration);
        duration /= NIS_FrameRate;                   //convert from frames to seconds
#if NIS_ERROR_CHECKING
        dbgAssert(nScanned == 2);
        dbgAssert(duration > 0.0f && duration < 300.0f);
#endif
    }
    else
    {
        nScanned = sscanf(field, "%f,%f", &distance, &duration);
#if NIS_ERROR_CHECKING
        dbgAssert(nScanned == 2);
        dbgAssert(duration > 0.0f && duration < 300.0f);
#endif
    }
    event->param[0] = TreatAsUdword(distance);
    event->param[1] = TreatAsUdword(duration);
}
void nisTextFadeSet(char *directory, char *field, void *dataToFillIn)
{
    sdword nScanned;
    real32 fade;

    _strupr(field);
    if (strstr(field, "FRAME"))
    {
        nScanned = sscanf(field, "%f FRAME", &fade);
#if NIS_ERROR_CHECKING
        dbgAssert(nScanned == 1);
#endif
        fade /= NIS_FrameRate;                   //convert from frames to seconds
    }
    else
    {
        nScanned = sscanf(field, "%f", &fade);
#if NIS_ERROR_CHECKING
        dbgAssert(nScanned == 1);
#endif
    }
    if (dataToFillIn != NULL)
    {
        nisCurrentTextFadeOut = fade;
    }
    else
    {
        nisCurrentTextFadeIn = fade;
    }
}
void nisTextMarginSet(char *directory, char *field, void *dataToFillIn)
{
    sdword nScanned;

    nScanned = sscanf(field, "%d", &nisCurrentTextMargin);
#if NIS_ERROR_CHECKING
    dbgAssert(nScanned == 1);
#endif
}
void nisTextDurationSet(char *directory, char *field, void *dataToFillIn)
{
    sdword nScanned;
    real32 value;

    _strupr(field);
    if (strstr(field, "FRAME"))
    {
        nScanned = sscanf(field, "%f FRAME", &value);
#if NIS_ERROR_CHECKING
        dbgAssert(nScanned == 1);
#endif
        value /= NIS_FrameRate;                   //convert from frames to seconds
    }
    else
    {
        nScanned = sscanf(field, "%f", &value);
#if NIS_ERROR_CHECKING
        dbgAssert(nScanned == 1);
#endif
    }
    nisCurrentDuration = value;
}
void nisTextCardSet(char *directory, char *field, void *dataToFillIn)
{
    nisevent *event = nisNewEventNoObject(NEO_TextCard);
    nistextcard *card;
    char *newLine, *lineStart;
    fonthandle fhSave = fontCurrentGet();
    char *text;
    strGamesMessages message;

    //find the text we are to draw
    message = strNameToEnum(field);

    dbgAssert(message != NumStrings);
    text = strGetString(message);
    //clear out quotes from the text.  I know this is modifying the string but we should get rid of bracketing quotes.
    while (*text == '"')
    {
        text++;
    }
    while (strlen(text) > 1 && text[strlen(text) - 1] == '"')
    {
        text[strlen(text) - 1] = 0;
    }
    //allocate text card and string
    card = memAlloc(sizeof(nistextcard) + strlen(text) + 1, "TextCard", NonVolatile);

    card->duration = nisCurrentDuration;
    card->c = nisCurrentTextCardColor;
    card->font = nisCurrentTextCardFont;
    card->x = nisCurrentTextCardX;
    card->y = nisCurrentTextCardY;
    card->fadeIn = nisCurrentTextFadeIn;
    card->fadeOut = nisCurrentTextFadeOut;
    card->margin = nisCurrentTextMargin;
    strcpy((char *)(card + 1), text);
    card->text = (char *)(card + 1);

    fontMakeCurrent(nisCurrentTextCardFont);
    lineStart = card->text;
    newLine = strstr(lineStart, NIS_NewLine);
    while (newLine != NULL)
    {
        nisCurrentTextCardY += fontHeight(" ") + 1;
        nisCurrentTextCardX = nisCurrentTextMargin + fontWidthN(newLine + NIS_NewLineLength, newLine - lineStart);
        lineStart = newLine + NIS_NewLineLength;
        newLine = strstr(lineStart, NIS_NewLine);
    }
    nisCurrentTextCardX += fontWidth(lineStart);

    event->param[0] = (udword)card;
    fontMakeCurrent(fhSave);
}
void nisCameraBlendInSet(char *directory, char *field, void *dataToFillIn)
{
    sdword nScanned;
    real32 time0, time1 = -1.0f;
    char *string0, *string1;

    _strupr(field);
    string0 = strtok(field, " \n\t,");
    string1 = strtok(NULL, " \n\t,");
    //read in the first time
    dbgAssert(string0);
    if (string1 != NULL && strstr(string1, "FRAME"))
    {
        nScanned = sscanf(string0, "%f", &time0);
#if NIS_ERROR_CHECKING
        dbgAssert(nScanned == 1);
#endif
        time0 /= NIS_FrameRate;                   //convert from frames to seconds
        string0 = strtok(NULL, " \n\t,");
        string1 = strtok(NULL, " \n\t,");
    }
    else
    {
        nScanned = sscanf(field, "%f", &time0);
#if NIS_ERROR_CHECKING
        dbgAssert(nScanned == 1);
#endif
        string0 = string1;
        string1 = strtok(NULL, " \n\t,");
    }
    //read in the second time, if there was one
    if (string0)
    {
        if (string1 != NULL && strstr(string1, "FRAME"))
        {
            nScanned = sscanf(string0, "%f", &time1);
#if NIS_ERROR_CHECKING
            dbgAssert(nScanned == 1);
#endif
            time1 /= NIS_FrameRate;                   //convert from frames to seconds
        }
        else
        {
            nScanned = sscanf(field, "%f", &time1);
#if NIS_ERROR_CHECKING
            dbgAssert(nScanned == 1);
#endif
        }
    }
#if NIS_ERROR_CHECKING
    if (time0 <= 0.0f || time0 > 400.4f || (time1 != -1.0f && (time1 <= 0.0f || time1 > 400.4f)))
    {
        dbgFatalf(DBG_Loc, "Error scanning '%s'", field);
    }
#endif
    nisCameraCutTime = time0;
    nisCameraCutStage2 = time1;
}
/*
void nisCameraBlendOutSet(char *directory, char *field, void *dataToFillIn)
{
    nisCameraCutOut = TRUE;
}
*/
void nisScissorBlendSet(char *directory, char *field, void *dataToFillIn)
{
    sdword nScanned;
    real32 value;
    nisevent *event;

    _strupr(field);
    if (strstr(field, "FRAME"))
    {
        nScanned = sscanf(field, "%f FRAME", &value);
#if NIS_ERROR_CHECKING
        dbgAssert(nScanned == 1);
#endif
        value /= NIS_FrameRate;                   //convert from frames to seconds
    }
    else
    {
        nScanned = sscanf(field, "%f", &value);
#if NIS_ERROR_CHECKING
        dbgAssert(nScanned == 1);
#endif
    }
#if NIS_ERROR_CHECKING
    if (nScanned != 1 || value <= 0.0f || value > 400.4f)
    {
        dbgFatalf(DBG_Loc, "Error scanning '%s'", field);
    }
#endif
    if (dataToFillIn == NULL)
    {
        nisScissorFadeIn = value;
    }
    else
    {
        event = nisNewEventNoObject(NEO_ScissorOut);
        event->param[0] = TreatAsUdword(value);
    }
}
void nisFocusAtEndSet(char *directory,char *field,void *dataToFillIn)
{
    nisNewEvent(NEO_FocusAtEnd);
}
void nisFocusSet(char *directory,char *field,void *dataToFillIn)
{
    nisNewEventNoObject(NEO_Focus);
}
void nisMusicStartSet(char *directory,char *field,void *dataToFillIn)
{
    sdword trackNumber, nScanned;
    nisevent *event = nisNewEventNoObject(NEO_StartMusic);

    nScanned = sscanf(field, "%d", &trackNumber);
#if NIS_ERROR_CHECKING
    if (nScanned != 1 || trackNumber < 0 || trackNumber > 99)
    {
        dbgFatalf(DBG_Loc, "Error scanning '%s' for a track number.", field);
    }
#endif
    event->param[0] = trackNumber;
}
void nisMusicStopSet(char *directory,char *field,void *dataToFillIn)
{
    real32 fadeOut;
    sdword nScanned;
    nisevent *event = nisNewEventNoObject(NEO_StopMusic);

    _strupr(field);
    if (strstr(field, "FRAME"))
    {
        nScanned = sscanf(field, "%f FRAME", &fadeOut);
#if NIS_ERROR_CHECKING
        dbgAssert(nScanned == 1);
#endif
        fadeOut /= NIS_FrameRate;                   //convert from frames to seconds
    }
    else
    {
        nScanned = sscanf(field, "%f", &fadeOut);
#if NIS_ERROR_CHECKING
        dbgAssert(nScanned == 1);
#endif
    }
#if NIS_ERROR_CHECKING
    if (nScanned != 1 || fadeOut < 0.0f || fadeOut > 99.0f)
    {
        dbgFatalf(DBG_Loc, "Error scanning '%s' for a fade-out.", field);
    }
#endif
    event->param[0] = TreatAsUdword(fadeOut);
}
void nisCentreShipSet(char *directory,char *field,void *dataToFillIn)
{
    dbgAssert(nisCurrentObject >= 0);
    dbgAssert(nisCurrentObject < nisCurrentHeader->nObjectPaths);
    bitSet(nisCurrentHeader->objectPath[nisCurrentObject].race, NIS_CentreShip);
}
void nisFadeSetSet(char *directory,char *field,void *dataToFillIn)
{
    nisevent *event = nisNewEventNoObject(NEO_BlackFadeSet);
    real32 level;
    sdword nScanned;

    nScanned = sscanf(field, "%f", &level);
    if (strchr(field, '%'))
    {                                                   //if it's a percentage
        level /= 100.0f;
    }
#if NIS_ERROR_CHECKING
    dbgAssert(nScanned == 1);
    if (level < 0.0f || level > 1.0f)
    {
        dbgFatalf(DBG_Loc, "'%s' is not in the range of 0..1", field);
    }
#endif
    event->param[0] = TreatAsUdword(level);
}
void nisFadeToSet(char *directory,char *field,void *dataToFillIn)
{
    nisevent *event = nisNewEventNoObject(NEO_BlackFadeTo);
    real32 level, duration;
    sdword nScanned;
    char *time;

    time = strchr(field, ',');
    dbgAssert(time);
    *time = 0;
    time++;
    nScanned = sscanf(field, "%f", &level);
    if (strchr(field, '%'))
    {                                                   //if it's a percentage
        level /= 100.0f;
    }
#if NIS_ERROR_CHECKING
    if (level < 0.0f || level > 1.0f)
    {
        dbgFatalf(DBG_Loc, "'%s' is not in the range of 0..1", field);
    }
#endif
    _strupr(time);
    if (strstr(time, "FRAME"))
    {
        nScanned = sscanf(time, "%f FRAME", &duration);
#if NIS_ERROR_CHECKING
        dbgAssert(nScanned == 1);
#endif
        duration /= NIS_FrameRate;                   //convert from frames to seconds
    }
    else
    {
        nScanned = sscanf(time, "%f", &duration);
#if NIS_ERROR_CHECKING
        dbgAssert(nScanned == 1);
#endif
    }
#if NIS_ERROR_CHECKING
    if (nScanned != 1 || duration <= 0.0f || duration > 400.4f)
    {
        dbgFatalf(DBG_Loc, "Error scanning '%s'", time);
    }
#endif
    event->param[0] = TreatAsUdword(level);
    event->param[1] = TreatAsUdword(duration);
}
void nisColorSchemeSet(char *directory, char *field, void *dataToFillIn)
{
    nisevent *event = nisNewEvent(NEO_ColorScheme);
    sdword nScanned, value;

    nScanned = sscanf(field, "%d", &value);
#if NIS_ERROR_CHECKING
    dbgAssert(nScanned == 1);
    dbgAssert(value >= 0 && value < MAX_MULTIPLAYER_PLAYERS);
#endif
    event->param[0] = value;
}
void nisMeshAnimationStartSet(char *directory, char *field, void *dataToFillIn)
{
    nisevent *event = nisNewEvent(NEO_MeshAnimationStart);

    event->param[0] = (udword)memStringDupe(field);
}
void nisMeshAnimationStopSet(char *directory, char *field, void *dataToFillIn)
{
    nisevent *event = nisNewEvent(NEO_MeshAnimationStop);
}
void nisMeshAnimationPauseSet(char *directory, char *field, void *dataToFillIn)
{
    nisevent *event = nisNewEvent(NEO_MeshAnimationPause);
}
void nisMeshAnimationSeekSet(char *directory, char *field, void *dataToFillIn)
{
    nisevent *event = nisNewEvent(NEO_MeshAnimationSeek);
    real32 seekTime;
    sdword nScanned;

    _strupr(field);
    if (strstr(field, "FRAME"))
    {
        nScanned = sscanf(field, "FRAME %f", &seekTime);
#if NIS_ERROR_CHECKING
        dbgAssert(nScanned == 1);
#endif
        seekTime /= NIS_FrameRate;                   //convert from frames to seconds
    }
    else
    {
        nScanned = sscanf(field, "%f", &seekTime);
#if NIS_ERROR_CHECKING
        dbgAssert(nScanned == 1);
#endif
    }
#if NIS_ERROR_CHECKING
    if (nScanned != 1 || seekTime < 0.0f || seekTime > 300.0f)
    {
        dbgFatalf(DBG_Loc, "Error scanning '%s' for a seek time.", field);
    }
#endif
    event->param[0] = TreatAsUdword(seekTime);
}
void nisBlendCameraToFocusSet(char *directory, char *field, void *dataToFillIn)
{
    nisNewEventNoObject(NEO_BlendCameraEndPoint);
}
void nisVolumesSet(char *directory, char *field, void *dataToFillIn)
{
    sdword nScanned;
    float level;
    nisevent *event = nisNewEventNoObject((udword)dataToFillIn);

    nScanned = sscanf(field, "%f", &level);

    dbgAssert(nScanned == 1);
    dbgAssert(level >= 0.0f && level <= 1.0f);
    event->param[0] = TreatAsUdword(level);
}
void nisTrailZeroSet(char *directory, char *field, void *dataToFillIn)
{
    nisNewEvent(NEO_TrailZeroLength);
}
void nisTrailMoveSet(char *directory, char *field, void *dataToFillIn)
{
    nisNewEvent(NEO_TrailMove);
}
void nisUniversePauseSet(char *directory, char *field, void *dataToFillIn)
{
    nisNewEventNoObject(NEO_UniversePauseToggle);
}
void nisUniverseHideSet(char *directory, char *field, void *dataToFillIn)
{
    nisNewEventNoObject(NEO_UniverseHideToggle);
}
void nisNewEnvironmentSet(char *directory, char *field, void *dataToFillIn)
{
    nisevent *event = nisNewEventNoObject(NEO_NewEnvironment);
    char *name, *scan;
    nisenvironment *env = memAlloc(sizeof(nisenvironment), "NISEnv", NonVolatile);

    name = strtok(field, " \n\t,");
    dbgAssert(name);
    dbgAssert(strlen(name) < NIS_EnvNameLength);
    strcpy(env->name, name);
    scan = strtok(NULL, " \n\t,");
    dbgAssert(scan);
    sscanf(scan, "%f", &env->theta);
    scan = strtok(NULL, " \n\t,");
    dbgAssert(scan);
    sscanf(scan, "%f", &env->phi);
    event->param[0] = (udword)env;
}
void nisLookyObjectSet(char *directory, char *field, void *dataToFillIn)
{
    dbgAssert(nisCurrentObject != -1);

    nisCurrentHeader->iLookyObject = nisCurrentObject;
}
//scan in formatted like this:
//<hh>:<mm>:<ss>:<ff> <x> <y> [<bRightJustified>] [<red>] [<green>] [<blue>] [<font>] [<format>]
//defaults are:                 ^TRUE^             ^255^    ^255^     ^255^  ^default^  ^"%s"^
void nisSMPTEOnSet(char *directory, char *field, void *dataToFillIn)
{
    char timeString[80];
    char formatString[80] = "%s";
    char fontString[80] = "default.hff";
    char justifyString[80] = "TRUE";
    ubyte red = UBYTE_Max, green = UBYTE_Max, blue = UBYTE_Max;
    sdword x, y;
    sdword nScanned;
    real32 hours, minutes, seconds, frames;
    nisSMPTE *newSMPTE;
    nisevent *event = nisNewEventNoObject(NEO_SMPTEOn);

    //scan in all the parameters
    RemoveCommasFromString(field);
    nScanned = sscanf(field, "%s %d %d %s %d %d %d %s %s", timeString, &x, &y, &red, &green, &blue, fontString, formatString);
    dbgAssert(nScanned >= 3);
    dbgAssert(strlen(formatString) > 1);

    //interpret the SMPTE timecode
    nScanned = sscanf(timeString, "%f:%f:%f:%f", &hours, &minutes, &seconds, &frames);
    dbgAssert(nScanned == 4);
    dbgAssert(hours >= 0 && hours < 24);
    dbgAssert(minutes >= 0 && minutes < 60);
    dbgAssert(seconds >= 0 && seconds < 60);
    dbgAssert(frames >= 0 && frames < NIS_FrameRate);

    //allocate parameter block and format string
    newSMPTE = memAlloc(sizeof(nisSMPTE) + strlen(formatString) + 1, "nisSMPTE", NonVolatile);
    strcpy((char *)(newSMPTE + 1), formatString);
    newSMPTE->format = (char *)(newSMPTE + 1);              //point to end of structure, where the format string is stored
    newSMPTE->x = x * MAIN_WindowWidth / 640;
    newSMPTE->y = y * MAIN_WindowHeight / 480;
    newSMPTE->bRightJustified = scriptStringToBool(justifyString);
    newSMPTE->startTime = nisCurrentTime - (hours * 3600 + minutes * 60 + seconds + frames / NIS_FrameRate);
    newSMPTE->font = frFontRegister(fontString);
    newSMPTE->c = colRGB(red, green, blue);
    event->param[0] = (udword)newSMPTE;
}
void nisSMPTEOffSet(char *directory, char *field, void *dataToFillIn)
{
    nisNewEventNoObject(NEO_SMPTEOff);
}
//scan a string formatted like this:
//nLines, nLinesVar, width, widthVar, y, yVar[, hue, hueVar[, lum, lumVar[, sat, satVar[, alpha, alphaVar[, bTextured]]]]
//defaults are:                                 ^0^   ^0^     ^1^   ^0^     ^0^   ^0^      ^1^     ^0^       ^FALSE^
void nisStaticOnSet(char *directory, char *field, void *dataToFillIn)
{
    sdword nScanned;
    nisstatic *newStatic;
    sdword nLines, nLinesVariation, index;
    real32 width, widthVariation, y, yVariation;
    real32 hue = 0.0f, hueVariation = 0.0f, lum = 1.0f, lumVariation = 0.0f;
    real32 sat = 0.0f, satVariation = 0.0f, alpha = 1.0f, alphaVariation = 0.0f;
    nisevent *event = nisNewEventNoObject(NEO_StaticOn);
    char bTextureString[80] = "TRUE";

    RemoveCommasFromString(field);
    nScanned = sscanf(field, "%d %d %d %f %f %f %f %f %f %f %f %f %f %f %f %s", &index,
        &nLines, &nLinesVariation, &width, &widthVariation, &y, &yVariation,
        &hue, &hueVariation, &lum, &lumVariation, &sat, &satVariation, &alpha, &alphaVariation,
        bTextureString);
    dbgAssert(nScanned >= 7);
    dbgAssert(index >= 0 && index < NIS_NumberStatics);
    dbgAssert(hue >= 0.0f && hue <= 1.0f);
    dbgAssert(lum >= 0.0f && lum <= 1.0f);
    dbgAssert(sat >= 0.0f && sat <= 1.0f);
    dbgAssert(alpha >= 0.0f && alpha <= 1.0f);

    //scale some parameters by screen resolution
    width = primScreenToGLScaleX(width * MAIN_WindowWidth / 640.0f);
    widthVariation = primScreenToGLScaleX(widthVariation * MAIN_WindowWidth / 640.0f);
    y = primScreenToGLY(y * MAIN_WindowHeight / 480.0f);
    yVariation = primScreenToGLScaleY(yVariation * MAIN_WindowHeight / 480.0f);
    nLines = nLines * MAIN_WindowHeight / 480;              //more lines on a taller screen
    nLinesVariation = nLinesVariation * MAIN_WindowHeight / 480;
    newStatic = memAlloc(sizeof(nisstatic), "nisstatic", NonVolatile);
    newStatic->index = index;
    newStatic->nLines = nLines;
    newStatic->nLinesVariation = nLinesVariation;
    newStatic->width = width;
    newStatic->widthVariation = widthVariation;
    newStatic->y = y;
    newStatic->yVariation = yVariation;
    newStatic->hue = hue;
    newStatic->hueVariation = hueVariation;
    newStatic->lum = lum;
    newStatic->lumVariation = lumVariation;
    newStatic->sat = sat;
    newStatic->satVariation = satVariation;
    newStatic->alpha = alpha;
    newStatic->alphaVariation = alphaVariation;
    newStatic->bAlpha = (alpha != 1.0f || alphaVariation != 0.0f);
    newStatic->bTextured = scriptStringToBool(bTextureString);
    event->param[0] = (udword)newStatic;
}
void nisStaticOffSet(char *directory, char *field, void *dataToFillIn)
{
    nisevent *event = nisNewEventNoObject(NEO_StaticOff);
    sdword nScanned;

    nScanned = sscanf(field, "%d", &event->param[0]);
    dbgAssert(nScanned == 1);
    dbgAssert(event->param[0] >= 0 && event->param[0] < NIS_NumberStatics);
}
void nisHideDerelictSet(char *directory, char *field, void *dataToFillIn)
{
    nisevent *event = nisNewEventNoObject(dataToFillIn == NULL ? NEO_HideDerelict : NEO_ShowDerelict);
    DerelictType type = StrToDerelictType(field);

    dbgAssert(type != -1);
    event->param[0] = type;
}
void nisNewNISSet(char *directory, char *field, void *dataToFillIn)
{
    nisevent *event = nisNewEventNoObject(NEO_StartNewNIS);
    char nisName[80], scriptName[80];
    char fileName[80];
    sdword nScanned;

    event->time -= UNIVERSE_UPDATE_PERIOD;                  //this is to make sure that hitting ESCAPE will seek over this event
    nScanned = sscanf(field, "%s %s", nisName, scriptName);
    dbgAssert(nScanned == 2);
    dbgAssert(strlen(nisName) > 0);
    dbgAssert(strlen(scriptName) > 0);
    strcpy(fileName, "nis\\");
    strcat(fileName, nisName);
    dbgAssert(fileExists(fileName, 0));
    event->param[0] = (udword)memStringDupeNV(fileName);
    strcpy(fileName, "nis\\");
    strcat(fileName, scriptName);
    dbgAssert(fileExists(fileName, 0));
    event->param[1] = (udword)memStringDupeNV(fileName);
}
void nisAttributesSet(char *directory, char *field, void *dataToFillIn)
{
    sdword nScanned, attributes;
    nisevent *event = nisNewEvent(NEO_Attributes);

    nScanned = sscanf(field, "%d", &attributes);
    dbgAssert(nScanned == 1);
    event->param[0] = attributes;
}
void nisAttackKasSelectionSet(char *directory, char *field, void *dataToFillIn)
{
    nisevent *event = nisNewEvent(NEO_AttackKasSelection);
    GrowSelection *selection;

    selection = kasGetGrowSelectionPtrIfExists(field);      //get a named selection
    dbgAssert(selection != NULL);
    dbgAssert(selection->selection->numShips > 0);          //verify there are ships in it
    event->param[0] = (udword)(selection->selection);
}
void nisKeepMovingAtEndSet(char *directory, char *field, void *dataToFillIn)
{
    nisNewEvent(NEO_KeepMovingAtEnd);
}

void nisMinimumLODSet(char *directory, char *field, void *dataToFillIn)
{
    nisevent *event = nisNewEvent(NEO_MinimumLOD);
    sdword nScanned;

    nScanned = sscanf(field, "%d", &event->param[0]);
    dbgAssert(nScanned == 1);
    dbgAssert(event->param[0] >= 0 && event->param[0] < 10);
}

void nisDamageLevelSet(char *directory, char *field, void *dataToFillIn)
{
    nisevent *event = nisNewEvent(NEO_DamageLevel);
    sdword nScanned;
    real32 percentage;

    nScanned = sscanf(field, "%f", &percentage);
    dbgAssert(nScanned == 1);
    if (strchr(field, '%%'))
    {
        percentage /= 100.0f;
    }
    dbgAssert(percentage > 0.0f);
    event->param[0] = TreatAsUdword(percentage);
}

scriptEntry nisScriptTable[] =
{
    { "with",           nisCurrentObjectSet, NULL},
    { "at",             nisCurrentTimeSet, NULL},
    { "}",              nisCurrentObjectClear, NULL},
    { "deathDamage",    nisDeathDamageSet, NULL},
    { "deathProjectile",nisDeathProjectileSet, NULL},
    { "deathBeam",      nisDeathBeamSet, NULL},
    { "attack",         nisAttackSet, NULL},
    { "haltAttack",     nisHaltAttackSet, NULL},
    { "fire",           nisFireSet, NULL},
    { "hide",           nisCurrentObjectHide, NULL},
    { "show",           nisCurrentObjectShow, NULL},
    { "invincible",     nisInvincibleSet, NULL},
    { "vincible",       nisVincibleSet, NULL},
    { "soundEvent",     nisSoundEventSet, NULL},
    { "speechEvent",    nisSpeechEventSet, NULL},
    { "fleetSpeechEvent", nisFleetSpeechEventSet, NULL},
    { "animaticSpeechEvent", nisAnimaticSpeechEventSet, NULL},
    { "startMusic",     nisMusicStartSet, NULL},
    { "stopMusic",      nisMusicStopSet, NULL},
    { "remainAtEnd",    nisRemainAtEndSet, NULL},
    { "cameraFOV",      nisCameraFOVSet, NULL},
    { "swapRaces",      nisRaceSwap, NULL},
    { "cameraCut",      nisCameraCutSet, NULL},
//    { "startBlackScreen",nisBlackScreenSet, NULL},
//    { "endBlackScreen", nisEndBlackScreenSet, NULL},
    { "fadeSet",        nisFadeSetSet, NULL},
    { "fadeTo",         nisFadeToSet, NULL},
    { "colorScheme",    nisColorSchemeSet, NULL},
    { "textFont",       nisTextFontSet, NULL},
    { "textColor",      nisTextColorSet, NULL},
    { "textPosition",   nisTextPositionSet, NULL},
    { "textMargin",     nisTextMarginSet, NULL},
    { "textDuration",   nisTextDurationSet, NULL},
    { "textScroll",     nisTextScrollSet, NULL},
    { "textFadeIn",     nisTextFadeSet, NULL},
    { "textFadeOut",    nisTextFadeSet, (void *)TRUE},
    { "textCard",       nisTextCardSet, NULL},
    { "cameraBlendIn",  nisCameraBlendInSet, NULL},
//    { "cameraBlendOut", nisCameraBlendOutSet, NULL},
    { "wideScreenIn",   nisScissorBlendSet, NULL},
    { "wideScreenOut",  nisScissorBlendSet, (void *)TRUE},
    { "focusAtEnd",     nisFocusAtEndSet, (void *)TRUE},
    { "focus",          nisFocusSet, (void *)TRUE},
    { "centreShip",     nisCentreShipSet, NULL},
    { "meshAnimStart",  nisMeshAnimationStartSet, NULL},
    { "meshAnimStop",   nisMeshAnimationStopSet, NULL},
    { "meshAnimPause",  nisMeshAnimationPauseSet, NULL},
    { "meshAnimSeek",   nisMeshAnimationSeekSet, NULL},
    { "blendCameraToFocus",   nisBlendCameraToFocusSet, NULL},
    { "musicVolume",    nisVolumesSet, (void *)NEO_MusicVolume},
    { "speechVolume",   nisVolumesSet, (void *)NEO_SpeechVolume},
    { "SFXVolume",      nisVolumesSet, (void *)NEO_SFXVolume},
    { "trailMove",      nisTrailMoveSet, NULL},
    { "trailZero",      nisTrailZeroSet, NULL},
    { "pauseUniverseToggle",  nisUniversePauseSet, NULL},
    { "hideUniverseToggle",  nisUniverseHideSet, NULL},
    { "environment",    nisNewEnvironmentSet, NULL},
    { "midPointLookAt", nisLookyObjectSet, NULL},
    { "SMPTEOn",        nisSMPTEOnSet, NULL},
    { "SMPTEOff",       nisSMPTEOffSet, NULL},
    { "staticOn",       nisStaticOnSet, NULL},
    { "staticOff",      nisStaticOffSet, NULL},
    { "hideDerelict",   nisHideDerelictSet, NULL},
    { "showDerelict",   nisHideDerelictSet, (void *)TRUE},
    { "newNIS",         nisNewNISSet, NULL},
    { "attributes",     nisAttributesSet, NULL},
    { "attackKasSelection",nisAttackKasSelectionSet, NULL},
    { "keepMovingAtEnd",nisKeepMovingAtEndSet, NULL},
    { "numberLODs",     nisMinimumLODSet, NULL},
    { "health",         nisDamageLevelSet, NULL},
    { NULL, NULL, 0 }
};

/*-----------------------------------------------------------------------------
    Name        : nisCompareFunc
    Description : For sorting the object lists by parent name
    Inputs      :
    Outputs     :
    Return      :
----------------------------------------------------------------------------*/
int nisCompareFunc(const void *p1, const void *p2)
{
    return((udword)(((spaceobjpath *)p1)->parentIndex) - (udword)(((spaceobjpath *)p2)->parentIndex));
}

/*-----------------------------------------------------------------------------
    Name        : nisEventSortCB
    Description : For sorting the event lists by time, subsorted by origional order
    Inputs      :
    Outputs     :
    Return      :
----------------------------------------------------------------------------*/
int nisEventSortCB(const void *p1, const void *p2)
{
    if (((nisevent *)p1)->time < ((nisevent *)p2)->time)
    {
        return(-1);
    }
    else if (((nisevent *)p1)->time == ((nisevent *)p2)->time)
    {
        if (((nisevent *)p1)->initialID < ((nisevent *)p2)->initialID)
        {
            return(-1);
        }
        else
        {
            dbgAssert(((nisevent *)p1)->initialID != ((nisevent *)p2)->initialID);
            return(1);
        }
    }
    else
    {
        return(1);
    }
}

/*-----------------------------------------------------------------------------
    Name        : nisGenericObjectRegister
    Description : Register and load, if needed, generic object as a derelict
                    into this NIS's local generic object registry.
    Inputs      : header - what NIS header to load object into
                  name - name of object to load
    Outputs     :
    Return      : index, in this NIS's registry of the newly loaded generic object.
----------------------------------------------------------------------------*/
sdword nisGenericObjectRegister(nisheader *header, char *name)
{
    sdword index;
    nisgenericobject *generic;
    char shipFile[256];

    for (index = 0; index < header->nGenericObjects; index++)
    {
        if (!strcmp(header->genericObject[index]->name, name))
        {
            return(index);
        }
    }
    //if it gets here, no match was found.  Load in a new object.
    header->genericObject = memRealloc(header->genericObject,//reallocate the list of generic objects
        sizeof(nisgenericobject **) * (header->nGenericObjects + 1), "NISGenericList", 0);
    generic = memAlloc(nisGenericObjectSize(strlen(name)), "NISGenericObject", 0);
    header->genericObject[header->nGenericObjects] = generic;
    header->nGenericObjects++;
    strcpy(generic->name, name);
    generic->staticInfo = memAlloc(sizeof(DerelictStaticInfo), "NISGenericStat", 0);
    strcpy(shipFile, name);
    strcat(shipFile, ".shp");
    InitStatDerelictInfoByPath(generic->staticInfo, NUM_DERELICTTYPES + index, "Misc\\", shipFile);
    return(header->nGenericObjects - 1);
}

/*-----------------------------------------------------------------------------
    Name        : nisLoad
    Description : Load in an NIS from file
    Inputs      : fileName - name of NIS file.
                  scriptName - name of file to read events from.
    Outputs     : Allocates memory for and returns the loaded NIS
    Return      :                           ^^^^^^^^^^^^^^^^^^^^
----------------------------------------------------------------------------*/
nisheader *nisLoad(char *fileName, char *scriptName)
{
    nisheader header, *newHeader;
    filehandle f;
    sdword index, j;
    spaceobjpath *objPath;
    camerapath *camPath;
    ShipType type;
    char *instancePtr;
    char string[256];
    char effectPath[256];
    char *pString;
    char localisedPath[256];
    sdword instanceID;

    f = fileOpen(fileName, 0);                              //open the file
    fileBlockRead(f, &header, sizeof(nisheader));           //read the header
#if NIS_ERROR_CHECKING
    if (strcmp(header.identifier, NIS_Identifier) != 0)
    {
        dbgFatalf(DBG_Loc, "Invalid NIS file: %s", fileName);
    }
    if (header.version != NIS_VersionNumber)
    {
        dbgFatalf(DBG_Loc, "Invalid version number in %s.  Expected 0x%x found 0x%x", fileName, NIS_VersionNumber, header.version);
    }
#endif                                                      //allocate the new header/NIS data minus strings
    newHeader = memAlloc((udword)header.stringBlock, "NISHeader", NonVolatile);
    memset(newHeader, 0, sizeof(nisheader));
    memcpy(newHeader, &header, sizeof(nisheader));         //make allocated copy of header
    fileBlockRead(f, (ubyte *)newHeader + sizeof(nisheader),//read in the main body of NIS
                  (sdword)header.stringBlock - sizeof(nisheader));
    //separately allocate and load in the string block
    newHeader->stringBlock = memAlloc(newHeader->stringBlockLength, "NISStrings", NonVolatile);
    fileBlockRead(f, newHeader->stringBlock, newHeader->stringBlockLength);
    fileClose(f);                                           //done with the file
    newHeader->flags = 0;
//    newHeader->name += (udword)newHeader->stringBlock;      //fixup file name
    (ubyte *)newHeader->objectPath += (udword)newHeader;//fixup the object motion path array
    //sort the object headers based upon their parentage
    qsort(newHeader->objectPath, newHeader->nObjectPaths, sizeof(spaceobjpath), nisCompareFunc);
    for (index = 0; index < newHeader->nObjectPaths; index++)
    {                                                       //for each object motion path
        objPath = &newHeader->objectPath[index];            //get pointer to this motion path
        (ubyte *)objPath->parameters += (udword)newHeader;  //fixup the various arrays
        (ubyte *)objPath->times += (udword)newHeader;
        for (j = 0; j < 6; j++)
        {
            (ubyte *)objPath->curve[j] += (udword)newHeader;
        }

#if NIS_NORMALIZE_ANGLES

        for (j = 3; j < 6; j++)
        {                                                   //for heading, pitch, bank
            sdword k;
            for (k = 0; k < newHeader->objectPath[index].nSamples; k++)
            {
                newHeader->objectPath[index].curve[j][k] = DEG_TO_RAD(newHeader->objectPath[index].curve[j][k]);
            }
        }

#endif
        //convert ship name to type
        strcpy(string, (char *)objPath->type + (udword)newHeader->stringBlock);
        if ((instancePtr = strchr(string, '(')) != NULL)
        {
            sscanf(instancePtr + 1, "%d", &instanceID);
            dbgAssert(instanceID > 0 && instanceID < SWORD_Max);
            *instancePtr = 0;
        }
        else
        {
            instanceID = 0;
        }
        objPath->instance = instanceID;
        switch (objPath->race)
        {
            case R1:                                            //any ship type
            case R2:
            case P1:
            case P2:
            case P3:
            case Traders:
                if (!_stricmp(string, "Missile"))
                {                                               //if they're creating a missile
                    type = objPath->race;
                    objPath->race = NSR_Missile;
                }
                else
                {
                    type = StrToShipType(string);
                }
                break;
            case NSR_Asteroid:
                type = StrToAsteroidType(string);
                break;
            case NSR_DustCloud:
                type = StrToDustCloudType(string);
                break;
            case NSR_GasCloud:
                type = StrToGasCloudType(string);
                break;
            case NSR_Nebula:
                type = StrToNebulaType(string);
                break;
            case NSR_Derelict:
                type = StrToDerelictType(string);
                break;
            case NSR_Effect:
                sprintf(effectPath, "ETG\\%s.ebg", string);
                etgErrorRecoverable = FALSE;
                type = (ShipType)etgEffectCodeLoad(effectPath);
                break;
            case NSR_Generic:
                type = nisGenericObjectRegister(newHeader, string);
                break;
            default:;
#if NIS_ERROR_CHECKING
                dbgFatalf(DBG_Loc, "Invalid object race: %d", objPath->race);
#endif                                                      //allocate the new header/NIS data minus strings
        }
#if NIS_ERROR_CHECKING
        if (type == -1)
        {
            dbgFatalf(DBG_Loc, "Invalid object type '%s' of race %d", string, objPath->race);
        }
#endif                                                      //allocate the new header/NIS data minus strings
        objPath->type = type;                               //set new scanned ship type
        if (objPath->parentIndex != 0)
        {
            strcpy(string, (char *)objPath->parentIndex + (udword)newHeader->stringBlock);
            if ((instancePtr = strchr(string, '(')) != NULL)//if there is an instance
            {
                sscanf(instancePtr + 1, "%d", &instanceID);
                *instancePtr = 0;
            }
            else
            {
                instanceID = 0;
            }
            type = StrToShipType(string);
#if NIS_ERROR_CHECKING
            if (type == -1)
            {
                dbgFatalf(DBG_Loc, "Invalid parentIndex ship type '%s'", string);
            }
#endif                                                      //allocate the new header/NIS data minus strings
            for (j = 0; j < index; j++)
            {                                               //search for a parent
                if (newHeader->objectPath[j].type == type &&
                    newHeader->objectPath[j].instance == instanceID)
                {
                    objPath->parentIndex = j;
                    goto foundOne;
                }
            }
#if NIS_ERROR_CHECKING
            dbgFatalf(DBG_Loc, "Parent '%s' not found for object %d", string, index);
#endif
foundOne:;
        }
        else
        {
            objPath->parentIndex = -1;
        }
    }
    (ubyte *)newHeader->cameraPath += (udword)newHeader;    //fixup the object motion path array
    for (index = 0; index < newHeader->nCameraPaths; index++)
    {                                                       //for each object motion path
        camPath = &newHeader->cameraPath[index];            //get pointer to this motion path
        (ubyte *)camPath->parameters += (udword)newHeader;  //fixup the various arrays
        (ubyte *)camPath->times += (udword)newHeader;
        for (j = 0; j < 6; j++)
        {
            (ubyte *)camPath->curve[j] += (udword)newHeader;
        }

#if NIS_NORMALIZE_ANGLES
        for (j = 3; j < 6; j++)
        {                                                   //for heading, pitch, bank
            sdword k;
            for (k = 0; k < newHeader->cameraPath[index].nSamples; k++)
            {
                newHeader->cameraPath[index].curve[j][k] = DEG_TO_RAD(newHeader->cameraPath[index].curve[j][k]);
            }
        }
#endif
    }
    //!!! should also do the light lists

    newHeader->iLookyObject = -1;
    //load in the script file
    if (scriptName != NULL)
    {
        strcpy(localisedPath, scriptName);
        if (strCurLanguage >= 1)
        {
            for (pString = localisedPath + strlen(localisedPath); pString > localisedPath; pString--)
            {                                               //find the end of the path
                if (*pString == '\\')
                {
                    strcpy(string, pString + 1);            //save the file name
                    strcpy(pString + 1, nisLanguageSubpath[strCurLanguage]);//add the language sub-path
                    strcat(pString, string);                //put the filename back on
                    break;
                }
            }
        }
        nisEventIndex = 0;                                  //create temporary event list
        nisCurrentHeader = newHeader;
        scriptSet(NULL, localisedPath, nisScriptTable);     //load in the script file
        nisCurrentHeader = NULL;
        newHeader->nEvents = nisEventIndex;                 //create final event list in correct size
        if (newHeader->nEvents != 0)
        {
            newHeader->events = nisEventList;               //copy reference to event list
            qsort(newHeader->events, newHeader->nEvents, sizeof(nisevent), nisEventSortCB);//qsort this list by time
            nisEventList = NULL;                            //forget about this list
        }
        else
        {
            newHeader->events = NULL;
        }
    }
    else
    {
        newHeader->nEvents = 0;
        newHeader->events = NULL;
    }
    return(newHeader);
}

/*-----------------------------------------------------------------------------
    Name        : nisDelete
    Description : Delete an NIS header and all it's data.
    Inputs      : header - header to delete
    Outputs     :
    Return      : void
----------------------------------------------------------------------------*/
void nisDelete(nisheader *header)
{
    sdword index;

    dbgAssert(header != NULL);
    if (header->stringBlock != NULL)
    {                                                       //free the string blockl
        memFree(header->stringBlock);
    }
    if (header->events)
    {                                                       //free all the events
        for (index = 0; index < header->nEvents; index++)
        {
            switch (header->events[index].code)
            {                                               //any special-case freeing code goes here
                case NEO_StartNewNIS:
                    memFree((char *)header->events[index].param[1]);//free the text strings
                    // fall through
                case NEO_TextCard:
                case NEO_MeshAnimationStart:
                case NEO_NewEnvironment:
                case NEO_SMPTEOn:
                case NEO_StaticOn:
                    memFree((char *)header->events[index].param[0]);//free the text strings
                    break;
            }
        }
        memFree(header->events);
    }
    if (header->nGenericObjects != 0)
    {                                                       //if this NIS loaded generic objects
        for (index = 0; index < header->nGenericObjects; index++)
        {
            CloseStatDerelictInfo(header->genericObject[index]->staticInfo);//free the stat info and all meshes
            memFree(header->genericObject[index]->staticInfo);//free the statinfo memory
            memFree(header->genericObject[index]);          //free the name and stat info pointer
        }
        memFree(header->genericObject);                     //memfree the list
    }
    memFree(header);
}

/*-----------------------------------------------------------------------------
    Name        : nisShipStartMatrix
    Description : Find the starting vector for a ship in an NIS
    Inputs      : header - header to look in
                  race - race of ship
                  type - type of ship
                  instance - instance ID of that ship race/type
    Outputs     : destVector - starting position vector of ship to find or
                        0,0,0 if not found
                  destVector - starting matrix of ship to find or identity
                        if not found
    Return      : TRUE if found, FALSE otherwise
----------------------------------------------------------------------------*/
bool nisShipStartPosition(vector *destVector, matrix *destMatrix, nisheader *header, ShipRace race, ShipType type, sdword instance)
{
    spaceobjpath *path;
    sdword index, firstIndex = -1;
    vector rot;

    path = header->objectPath;
    for (index = 0; index < header->nObjectPaths; index++, path++)
    {                                                       //look at all object paths
        if (bitTest(path->race, NIS_CentreShip))
        {                                                   //if this ship explicitly specified as centre ship
            firstIndex = index;                             //use it over anything else
            break;
        }
        if (((instance == -1) || (path->instance == instance)) &&
            ((race == -1) || ((path->race & (~NIS_CentreShip)) == race)) &&
            ((type == -1) || (path->type == type)) )
        {                                                   //if matching object criteria
            if (firstIndex == -1)
            {
                firstIndex = index;
            }
        }
    }
    if (firstIndex != -1)
    {
        path = &header->objectPath[firstIndex];
        destVector->x = -path->curve[2][0];
        destVector->y = path->curve[0][0];
        destVector->z = path->curve[1][0];
        rot.x = path->curve[3][0];
        rot.y = path->curve[4][0];
        rot.z = path->curve[5][0];
        nisShipEulerToMatrix(destMatrix, &rot);
        return(TRUE);
    }
    //nothing found, return identity matrix and 0,0,0 vector
    destVector->x = destVector->y = destVector->z = 0.0f;
    *destMatrix = IdentityMatrix;
    return(FALSE);
}

/*-----------------------------------------------------------------------------
    Name        : nisTextCardListDraw
    Description : Draws the current list of text cards and expires old ones.
    Inputs      :
    Outputs     :
    Return      : void
----------------------------------------------------------------------------*/
void nisTextCardListDraw(void)
{
    sdword index;
    fonthandle fhSave = fontCurrentGet();
    char *lineStart, *newLine;
    sdword x, y;
    nistextcard *card;
    color c;
    real32 fadeValue;
    udword multiplier;
    sdword scroll;
    real32 scrollFloat;
    sdword screenCentreX = (MAIN_WindowWidth - 640) / 2;
    sdword screenCentreY = (MAIN_WindowHeight - 480) / 2;

    //scroll the text, if applicable
    if (nisTextScrollDistance != 0.0f && nisTextCardList[0].NIS->timeElapsed < nisTextScrollEnd)
    {
        scrollFloat = nisTextScrollDistance * (nisTextCardList[0].NIS->timeElapsed - nisTextScrollStart) / (nisTextScrollEnd - nisTextScrollStart);
        scroll = (sdword)scrollFloat;
    }
    else
    {
        nisTextScrollDistance = 0.0f;
        scroll = 0;
    }
    for (index = 0, card = nisTextCardList; index < nisTextCardIndex; index++, card++)
    {
        if (card->NIS->timeElapsed - card->creationTime > card->duration)
        {                                                   //if card elapses
//            dbgMessagef("XXXX%10s", nisTextCardList[index].text);
            if (nisTextCardIndex > 1)
            {
                nisTextCardList[index] = nisTextCardList[nisTextCardIndex - 1];
            }
            nisTextCardIndex--;
            index--;
            card = &nisTextCardList[index];
            continue;
        }
        fontMakeCurrent(card->font);

        lineStart = card->text;
        newLine = strstr(lineStart, NIS_NewLine);
        x = card->x;
        y = card->y + scroll;
        //scroll the text, if applicable
/*
        if (card->scroll != 0.0f)
        {                                                   //if the text is scrolling
            y += (sdword)(card->scroll *                    //move to scrolled-to position
                (card->NIS->timeElapsed - card->creationTime) /
                card->duration);
        }
*/
        c = card->c;
        //fade the text in, if applicable
        if (card->fadeIn != 0.0f)
        {
            fadeValue = (card->NIS->timeElapsed - card->creationTime) / card->fadeIn;
            if (fadeValue >= 1.0f)
            {
                card->fadeIn = 0.0f;
                fadeValue = 1.0f;
            }
            multiplier = (udword)(fadeValue * 256.0f);
            c = colRGB(colRed(c) * multiplier / 256, colGreen(c) * multiplier / 256, colBlue(c) * multiplier / 256);
        }
        //fade the the text out, if applicable
        if (card->fadeOut != 0.0f)
        {                                                   //if there's a fadeout
            if (card->NIS->timeElapsed >= card->creationTime + card->duration - card->fadeOut)
            {                                               //if it in the fading part
                fadeValue = (card->NIS->timeElapsed - (card->creationTime + card->duration - card->fadeOut)) / card->fadeOut;
                fadeValue = max(0.0, 1.0f - fadeValue);
                multiplier = (udword)(fadeValue * 256.0f);
                c = colRGB(colRed(c) * multiplier / 256, colGreen(c) * multiplier / 256, colBlue(c) * multiplier / 256);
//                dbgMessagef("<0x%2x>", multiplier);
            }
        }
        //draw the text
        while (newLine != NULL)
        {
            fontPrintN(x, y, card->c, lineStart, newLine - lineStart);
            y += fontHeight(" ") + 1;
            x = card->margin;
            newLine += NIS_NewLineLength;
            lineStart = newLine;
            newLine = strstr(newLine, NIS_NewLine);
        }
        fontPrint(x + screenCentreX, y + screenCentreY, c, lineStart);
    }
    fontMakeCurrent(fhSave);
}

/*-----------------------------------------------------------------------------
    Name        : nisSMPTECounterDraw
    Description : Draw the specified SMPTE counter
    Inputs      : NIS - what NIS this counter belongs to, and where it will get
                    it's time counter.
    Outputs     :
    Return      : void
----------------------------------------------------------------------------*/
char *nisIStrings[] = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"};
void nisSMPTECounterDraw(nisplaying *NIS, nisSMPTE *SMPTE)
{
    real32 time;
    sdword integer;
    sdword hours, minutes, seconds, frames;
    char string[80] = "";
    char finalString[80];
    fonthandle oldFont;
    sdword x;

    time = NIS->timeElapsed - SMPTE->startTime;
    dbgAssert(time >= 0.0f);

    integer = (sdword)time;                                 //isolate the integer and fractional parts
    time -= (real32)integer;
    frames = (sdword)(time * NIS_FrameRate);
    dbgAssert(frames >= 0 && frames < (sdword)NIS_FrameRate);

    seconds = integer % 60;
    integer /= 60;
    dbgAssert(seconds >= 0 && seconds < 60);

    minutes = integer % 60;
    integer /= 60;
    dbgAssert(minutes >= 0 && minutes < 60);

    hours = integer;
    dbgAssert(hours >= 0 && hours < 60);

    //build the time string
    if (hours < 10)
    {
        strcat(string, "0");
        strcat(string, nisIStrings[hours]);
    }
    else
    {
        strcat(string, nisIStrings[hours / 10]);
        strcat(string, nisIStrings[hours % 10]);
    }
    strcat(string, ":");
    if (minutes < 10)
    {
        strcat(string, "0");
        strcat(string, nisIStrings[minutes]);
    }
    else
    {
        strcat(string, nisIStrings[minutes / 10]);
        strcat(string, nisIStrings[minutes % 10]);
    }
    strcat(string, ":");
    if (seconds < 10)
    {
        strcat(string, "0");
        strcat(string, nisIStrings[seconds]);
    }
    else
    {
        strcat(string, nisIStrings[seconds / 10]);
        strcat(string, nisIStrings[seconds % 10]);
    }
    strcat(string, ":");
    if (frames < 10)
    {
        strcat(string, "0");
        strcat(string, nisIStrings[frames]);
    }
    else
    {
        strcat(string, nisIStrings[frames / 10]);
        strcat(string, nisIStrings[frames % 10]);
    }
    sprintf(finalString, SMPTE->format, string);

    oldFont = fontCurrentGet();
    fontMakeCurrent(SMPTE->font);
    if (SMPTE->bRightJustified)
    {
        x = SMPTE->x - fontWidth(finalString);
    }
    else
    {
        x = SMPTE->x;
    }
    fontPrint(x, SMPTE->y, SMPTE->c, finalString);
    fontMakeCurrent(oldFont);
}

/*-----------------------------------------------------------------------------
    Name        : nisStaticDraw
    Description : Render some on-screen static
    Inputs      : snow - static parameters
    Outputs     :
    Return      : void
----------------------------------------------------------------------------*/
void nisStaticDraw(nisstatic *snow)
{
    sdword nLines;
    real32 y0, y1, x0, x1, width, s0, s1, t0;
    real32 top, bottom, shortest, longest;
    real32 one = primScreenToGLScaleY(1);
    real32 red, green, blue, alpha, hue, lum, sat;
    real32 minHue, maxHue, minLum, maxLum, minSat, maxSat, minAlpha, maxAlpha;

    shortest = (snow->width - snow->widthVariation) / 2.0f;
    longest = (snow->width + snow->widthVariation) / 2.0f;
    nLines = randyrandombetween(RAN_Static, snow->nLines - snow->nLinesVariation, snow->nLines + snow->nLinesVariation);
    top = snow->y - snow->yVariation;
    bottom = snow->y + snow->yVariation;
    if (snow->texture != TR_InvalidInternalHandle)
    {
        rndTextureEnable(TRUE);
        trRGBTextureMakeCurrent(snow->texture);
        if (snow->bAlpha)
        {                                                   //if it uses an alpha texture
            glEnable(GL_BLEND);
        }
        rndGLStateLog("Textured Static");
        glBegin(GL_QUADS);
        while (nLines)
        {
            y0 = frandyrandombetween(RAN_Static, top, bottom);
            y1 = y0 + one;
            width = frandyrandombetween(RAN_Static, shortest, longest);
            x0 = x1 = frandyrandombetween(RAN_Static, -1.0f, 1.0f);
            x0 -= width;
            x1 += width;
            nLines--;
            s0 = frandyrandom(RAN_Static, 1.0f);
            s1 = s0 + width * 2 * MAIN_WindowWidth / NIS_NoiseTextureWidth;
            t0 = frandyrandom(RAN_Static, 1.0f);

            glTexCoord2f(s0, t0);
            glVertex2f(x0, y1);
            glVertex2f(x0, y0);

            glTexCoord2f(s1, t0);
            glVertex2f(x1, y0);
            glVertex2f(x1, y1);

        }
    }
    else
    {                                                       //non-textured
        minHue = max(snow->hue - snow->hueVariation, 0.0f);
        maxHue = min(snow->hue + snow->hueVariation, 1.0f);
        minSat = max(snow->sat - snow->satVariation, 0.0f);
        maxSat = min(snow->sat + snow->satVariation, 1.0f);
        minLum = max(snow->lum - snow->lumVariation, 0.0f);
        maxLum = min(snow->lum + snow->lumVariation, 1.0f);
        minAlpha = max(snow->alpha - snow->alphaVariation, 0.0f);
        maxAlpha = min(snow->alpha + snow->alphaVariation, 1.0f);
        if (snow->bAlpha)
        {                                                   //if it uses an alpha texture
            glEnable(GL_BLEND);
        }
        glBegin(GL_LINES);
        while (nLines)
        {
            hue = frandyrandombetween(RAN_Static, minHue, maxHue);
            sat = frandyrandombetween(RAN_Static, minSat, maxSat);
            lum = frandyrandombetween(RAN_Static, minLum, maxLum);
            alpha = frandyrandombetween(RAN_Static, minAlpha, maxAlpha);
            colHLSToRGB(&red, &green, &blue, hue, lum, sat);
            y0 = frandyrandombetween(RAN_Static, top, bottom);
            width = frandyrandombetween(RAN_Static, shortest, longest);
            x0 = x1 = frandyrandombetween(RAN_Static, -1.0f, 1.0f);
            x0 -= width;
            x1 += width;
            nLines--;
            glColor4f(red, green, blue, alpha);
            glVertex2f(x0, y0);
            glVertex2f(x1, y0);
        }
    }
    glEnd();
    glDisable(GL_BLEND);
    rndTextureEnable(FALSE);
}

