/*============================================================================= Name : Demo.c Purpose : Code to record and play demos. Created 5/21/1998 by lmoloney Copyright Relic Entertainment, Inc. All rights reserved. =============================================================================*/ #include #include #include #include "types.h" #include "file.h" #include "memory.h" #include "debug.h" #include "key.h" #include "region.h" #include "mouse.h" #include "univupdate.h" #include "randy.h" #include "camera.h" #include "mainrgn.h" #include "select.h" #include "render.h" #include "netcheck.h" #include "demo.h" /*============================================================================= Data: =============================================================================*/ bool demDemoRecording = FALSE; bool demDemoPlaying = FALSE; bool wasDemoPlaying = FALSE; char demDemoFilename[128]; taskhandle demDemoTask; char demFileSaveName[128] = ""; demoheader *demLoadFile = NULL; sdword demFileSize; ubyte *demFilePointer; demplayfinished demFinishFunction = NULL; sdword nDemoPlays = 0, nUnivUpdates = 0; #if DEM_RANDOM_VERIFY bool8 demCheckStream[DEM_NumberStreamsToSave] = { 0, // RAN_ParticleStream 0, // RAN_ETG 0, // RAN_Sound 1, // RAN_Trails 1, // RAN_Clouds 1, // RAN_Nebulae 1, // RAN_AIPlayer 0, // RAN_Damage 1, // RAN_Battle 0, // RAN_Static 1, // RAN_Game 1, // RAN_SoundGameThread 0, // RAN_SoundBothThreads 1, // RAN_Trails0 1, // RAN_Trails1 1, // RAN_Trails2 1, // RAN_Trails3 1, // RAN_Trails4 1, // RAN_SoundGameThread0 1, // RAN_SoundGameThread1 1, // RAN_SoundGameThread2 1, // RAN_SoundGameThread3 1, // RAN_SoundGameThread4 1, // RAN_SoundGameThread5 1, // RAN_SoundGameThread6 }; #endif extern udword utyNFrameTicks; //auto-demo stuff #if DEM_AUTO_DEMO bool demAutoDemo = TRUE; real32 demAutoDemoWaitTime = DEM_AUTO_DELAY; #endif #if DEM_CHECKSUM bool demChecksumError = FALSE; char demChecksumString[DEM_ChecksumLength]; #endif #if DEM_INTERRUPTABLE bool demPlaybackInterrupted = FALSE; #endif //stuff for keeping track of fake vs. real renders udword demFakeRenderCount; //number of times we have to do fake renders before we're allowed to render again. udword demRecordedTicks; //accumulated number of ticks from recording udword demPlaybackTicks; //accumulated number of ticks from playback, including the remainder of last division udword demPacketNumber; //what demo 'packet' we're on #if DEM_FAKE_RENDER_SWITCH bool demFakeRenders = TRUE; //fake renders on by default #endif /*============================================================================= Functions: =============================================================================*/ /*----------------------------------------------------------------------------- Name : demBlockWrite Description : Write a block to the demo file. Inputs : Outputs : Return : ----------------------------------------------------------------------------*/ void demBlockWrite(void *data, sdword length) { FILE *appendfile; if (*demFileSaveName) { appendfile = fopen(demFileSaveName, "ab"); if (appendfile != NULL) { fwrite(data, length, 1, appendfile); fclose(appendfile); } } } /*----------------------------------------------------------------------------- Name : demRecordStart Description : Start recording a demo. Inputs : fileName - name of file to save to. saveFunction - user function to call to save user state info. Outputs : Return : void ----------------------------------------------------------------------------*/ void demRecordStart(char *fileName, demstatesave saveFunction) { demoheader header; ubyte *stateBuffer; #if DEM_RANDY_SAVE sdword index; #endif dbgAssert(demDemoPlaying != TRUE); //create a header structure strcpy(header.ident, DEM_HeaderString); header.version = DEM_VersionNumber; header.startTime = taskTimeElapsed; #if DEM_RANDY_SAVE for (index = 0; index < DEM_NumberStreamsToSave; index++) { //save the state of all random number streams ranParametersGet(index, &header.randyState[index].x, &header.randyState[index].y, &header.randyState[index].z, &header.randyState[index].c, &header.randyState[index].n); } #endif if (saveFunction != NULL) { //save initial configuration, if applicable saveFunction(&stateBuffer, &header.initialStateSize); dbgAssert(header.initialStateSize > 0); } else { header.initialStateSize = 0; } //open the file (actually, delete it, and every time we will open it in append write mode) strcpy(demFileSaveName,fileName); remove(fileName); demBlockWrite(&header, sizeof(header)); if (header.initialStateSize > 0) { //if there is state info to save demBlockWrite(stateBuffer, header.initialStateSize);//save it memFree(stateBuffer); //and free the state buffer } #if DEM_CHECKSUM if (logEnable == LOG_VERBOSE) { netlogfile = fopen(DEM_LogFileName, "wb"); fclose(netlogfile); netlogfile = NULL; } #endif demPacketNumber = 0; demStateSave(); demDemoRecording = TRUE; } /*----------------------------------------------------------------------------- Name : demStateSave Description : Save the state of the mouse and keyboard at a given instant. Inputs : void Outputs : Creates and saves a state structure. Return : void ----------------------------------------------------------------------------*/ void demStateSave(void) { demomaxstate state; sdword index, size; #if DEM_CHECKSUM sdword numShips; #endif dbgAssert(demDemoRecording); state.time = taskTimeElapsed; //basic state stuff #if DEM_CHECKSUM if (gameIsRunning) { if (logEnable == LOG_VERBOSE) { netlogfile = fopen(DEM_LogFileName, "at"); if (netlogfile) { fprintf(netlogfile, "************* Demo packet #%d Task time %.4f\n", demPacketNumber, taskTimeElapsed); } } state.univCheckSum = univGetChecksum(&numShips); if (netlogfile && logEnable == LOG_VERBOSE) { fclose(netlogfile); netlogfile = NULL; } #if DEM_CAMERA_CHECKSUM state.cameraCRC = cameraChecksum(mrCamera); #endif //DEM_CAMERA_CHECKSUM #if DEM_SELECT_CHECKSUM state.nSelected = selSelected.numShips; state.nSelecting = selSelecting.numTargets; #endif //DEM_SELECT_CHECKSUM } else { state.univCheckSum = DEM_NoChecksum; //no checksum when game not running } #if DEM_RANDOM_VERIFY for (index = 0; index < DEM_NumberStreamsToSave; index++) { if (demCheckStream[index]) { state.randyStream[index] = ranRandom(index); } } #endif //DEM_RANDOM_VERIFY #endif //DEM_CHECKSUM state.mouseX = (sword)mouseCursorX(); //save mouse state state.mouseY = (sword)mouseCursorY(); state.mouseButtons = (ubyte)mouseButtons; dbgAssert(taskNumberCalls >= 0 && taskNumberCalls < 256); state.taskNumber = (ubyte)taskNumberCalls; //save the value of the for loop in task.c where this is called from state.frameTicks = utyNFrameTicks; for (index = state.nKeys = 0; index < KEY_TOTAL_KEYS; index++) { //for all keys if (*((ubyte *)&keyScanCode[index]) != 0) { //if this scan code has some activity state.key[state.nKeys].index = (ubyte)index; //save it state.key[state.nKeys].key = keyScanCode[index]; state.nKeys++; //and increase the number of keys } } size = demStateSize(state.nKeys); demBlockWrite(&state, size); if (demoEndKeysHit()) { demRecordEnd(); } demPacketNumber++; } /*----------------------------------------------------------------------------- Name : demNumberTicksSave Description : Store the number of task ticks before any tasks are called Inputs : nTicks - number of task ticks for this time through Outputs : Return : ----------------------------------------------------------------------------*/ void demNumberTicksSave(sdword nTicks) { dbgAssert(demDemoRecording); demBlockWrite(&nTicks, sizeof(sdword)); } /*----------------------------------------------------------------------------- Name : demRecordEnd Description : End a demo recording session, closing the file Inputs : Outputs : Return : ----------------------------------------------------------------------------*/ void demRecordEnd(void) { dbgAssert(demDemoRecording); demFileSaveName[0] = 0; demDemoRecording = FALSE; } /*----------------------------------------------------------------------------- Name : demBlockRead Description : Read in a block of data from the demo file in memory. Inputs : Outputs : Return : ----------------------------------------------------------------------------*/ bool demBlockRead(void *dest, sdword size) { if (demFilePointer + size <= (ubyte *)demLoadFile + demFileSize) { memcpy(dest, demFilePointer, size); demFilePointer += size; return TRUE; } else { return FALSE; } } /*----------------------------------------------------------------------------- Name : demPlayStart Description : Start playing a demo Inputs : fileName - name of the file to start play loadfunction - function to call with state information finishFunction - function to call whent the demo playback is completed Outputs : Return : ----------------------------------------------------------------------------*/ void demPlayStart(char *fileName, demstateload loadFunction, demplayfinished finishFunction) { #if DEM_RANDY_SAVE sdword index; #endif dbgAssert(demDemoPlaying); dbgAssert(demLoadFile == NULL); demPlaybackInterrupted = FALSE; demFileSize = fileLoadAlloc(fileName, (void **)&demLoadFile, NonVolatile);//read in the whole file demFilePointer = (ubyte *)demLoadFile + sizeof(demoheader);//compute initial file pointer #if DEM_RANDY_SAVE for (index = 0; index < DEM_NumberStreamsToSave; index++) { //load the state of all random number streams ranParametersSet(index, demLoadFile->randyState[index].x, demLoadFile->randyState[index].y, demLoadFile->randyState[index].z, demLoadFile->randyState[index].c, demLoadFile->randyState[index].n); } #endif if (loadFunction != NULL && demLoadFile->initialStateSize > 0)//call the function to restore the configuration { loadFunction(demFilePointer, demLoadFile->initialStateSize); } demFilePointer += demLoadFile->initialStateSize; #if DEM_ERROR_CHECKING if (strcmp(demLoadFile->ident, DEM_HeaderString)) { dbgFatalf(DBG_Loc, "Invalid demo file '%s'", fileName); } if (demLoadFile->version != DEM_VersionNumber) { dbgFatalf(DBG_Loc, "Invalid version of file '%s'. Expected version 0x%x, found 0x%x.", fileName, DEM_VersionNumber, demLoadFile->version); } #endif #if DEM_CHECKSUM if (logEnable == LOG_VERBOSE) { netlogfile = fopen(DEM_LogFileName, "wb"); fclose(netlogfile); netlogfile = NULL; } #endif demFinishFunction = finishFunction; demFakeRenderCount = 0; demPlaybackTicks = 0; demRecordedTicks = 0; demPacketNumber = 0; demStateLoad(); } /*----------------------------------------------------------------------------- Name : demPlayEnd Description : Stop playing a demo Inputs : Outputs : Return : ----------------------------------------------------------------------------*/ void demPlayEnd(void) { rndMainViewRender = rndMainViewRenderFunction; dbgAssert(demDemoPlaying); memFree(demLoadFile); demLoadFile = NULL; demDemoPlaying = FALSE; #if DEM_CHECKSUM demChecksumError = FALSE; #endif if (demFinishFunction != NULL) { demFinishFunction(); demFinishFunction = NULL; } } /*----------------------------------------------------------------------------- Name : demStateLoad Description : Load in the state of the mouse and keyboard Inputs : Outputs : Return : void ----------------------------------------------------------------------------*/ void demStateLoad(void) { demomaxstate state; sdword index, size; #if DEM_CHECKSUM sdword numShips; real32 univCheckSum; #endif ubyte mouseLButton = keyScanCode[LMOUSE_BUTTON].keypressed; if (!demBlockRead(&state, demStateSize(0))) { //if there is no more file to read demPlayEnd(); //end of demo - hand control back to user return; } #if DEM_CHECKSUM strcpy(demChecksumString, "Demo Sync: "); demChecksumError = FALSE; if (gameIsRunning) { nDemoPlays++; if (logEnable == LOG_VERBOSE) { netlogfile = fopen(DEM_LogFileName, "at"); if (netlogfile) { fprintf(netlogfile, "************* Demo packet #%d Task time %.4f\n", demPacketNumber, taskTimeElapsed); } } univCheckSum = univGetChecksum(&numShips); if (netlogfile && logEnable == LOG_VERBOSE) { fclose(netlogfile); netlogfile = NULL; } if (state.univCheckSum != univCheckSum) { demChecksumError = TRUE; strcat(demChecksumString, "Univ "); } else { strcat(demChecksumString, " "); } #if DEM_CAMERA_CHECKSUM if (state.cameraCRC != cameraChecksum(mrCamera)) { demChecksumError = TRUE; strcat(demChecksumString, "Cam "); } else { strcat(demChecksumString, " "); } #endif //DEM_CAMERA_CHECKSUM #if DEM_SELECT_CHECKSUM if (state.nSelected != selSelected.numShips) { demChecksumError = TRUE; strcat(demChecksumString, "Sd "); } else { strcat(demChecksumString, " "); } if (state.nSelecting != selSelecting.numTargets) { demChecksumError = TRUE; strcat(demChecksumString, "Sg "); } else { strcat(demChecksumString, " "); } #endif //DEM_SELECT_CHECKSUM } #if DEM_RANDOM_VERIFY for (index = 0; index < DEM_NumberStreamsToSave; index++) { if (demCheckStream[index]) { if (ranRandom(index) != state.randyStream[index]) { demChecksumError = TRUE; sprintf(demChecksumString + strlen(demChecksumString), "R%c%c ", index >= 10 ? index/10 + '0' : '0', index%10 + '0'); } else { strcat(demChecksumString, " "); } } } #endif //DEM_RANDOM_VERIFY if (demChecksumError) { dbgMessagef("\n%s", demChecksumString); } #endif // DEM_CHECKSUM size = sizeof(demokey) * state.nKeys; if (!demBlockRead(&state.key, size)) { demPlayEnd(); return; } memset((ubyte *)&keyScanCode[0], 0, sizeof(keyScanCode)); for (index = 0; index < state.nKeys; index++) { //set state of all keys keyScanCode[state.key[index].index] = state.key[index].key; } mousePositionSet((sdword)state.mouseX, (sdword)state.mouseY);//set state of mouse mouseButtons = state.mouseButtons; taskNumberCalls = (udword)state.taskNumber; //make sure this one gets called just like when it was saved utyNFrameTicks = state.frameTicks; //tricky solution to get double-clicking to work in demo mode if (!mouseLButton && keyScanCode[LMOUSE_BUTTON].keypressed) { //if mouse button just hit mouseLClick(); } #if DEM_INTERRUPTABLE if (demoInterruptKeysHit()) { demPlaybackInterrupted = TRUE; demRecordEnd(); } #endif demPacketNumber++; } /*----------------------------------------------------------------------------- Name : demNumberTicksLoad Description : Load in the number of ticks for processing tasks, before any tasks get called. Inputs : initial - default number of ticks Outputs : Return : number of ticks for this process pass ----------------------------------------------------------------------------*/ sdword demNumberTicksLoad(sdword initial) { sdword nTicks; if (demLoadFile == NULL) { return(initial); } dbgAssert(demDemoPlaying); if (!demBlockRead(&nTicks, sizeof(sdword))) { demPlayEnd(); return(initial); } demRecordedTicks += nTicks; //update record/playback tick comparisons demPlaybackTicks += initial; if (demFakeRenderCount == 0) { //if we've rendered all the fake renders we were supposed to rndMainViewRender = rndMainViewRenderFunction; //do one regular render this time around #if DEM_FAKE_RENDER_SWITCH if (demRecordedTicks < demPlaybackTicks && (demFakeRenders)) #else if (demRecordedTicks < demPlaybackTicks) #endif { //if the recording was running faster than playback demFakeRenderCount = min(demPlaybackTicks / demRecordedTicks, DEM_MaxFakeRenders);//compute number of fake renders we'll need demPlaybackTicks = demPlaybackTicks % demRecordedTicks;//keep the remainder demRecordedTicks = 0; //reset the recording accumulator } else { //else playback is running fast enough! demFakeRenderCount = 1; demPlaybackTicks = 0; demRecordedTicks = 0; } } else { //already rendered once, set to render nothing if (gameIsRunning) { rndMainViewRender = rndMainViewAllButRenderFunction;//use the fake renderer next time around } } demFakeRenderCount--; //one less render, fake or real //because the last round of tasks may have set the render function pointer to something //else, let's set it back to the actual view rendering function. return(nTicks); }