1 /*
2  *  Recordings.c
3  *  Brogue
4  *
5  *  Created by Brian Walker on 8/8/10.
6  *  Copyright 2012. All rights reserved.
7  *
8  *  This program is free software: you can redistribute it and/or modify
9  *  it under the terms of the GNU Affero General Public License as
10  *  published by the Free Software Foundation, either version 3 of the
11  *  License, or (at your option) any later version.
12  *
13  *  This program is distributed in the hope that it will be useful,
14  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
15  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  *  GNU Affero General Public License for more details.
17  *
18  *  You should have received a copy of the GNU Affero General Public License
19  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
20  *
21  */
22 
23 #include <time.h>
24 #include <math.h>
25 #include <limits.h>
26 #include "Rogue.h"
27 #include "IncludeGlobals.h"
28 
29 #define RECORDING_HEADER_LENGTH     36  // bytes at the start of the recording file to store global data
30 
31 static const long keystrokeTable[] = {UP_ARROW, LEFT_ARROW, DOWN_ARROW, RIGHT_ARROW,
32     ESCAPE_KEY, RETURN_KEY, DELETE_KEY, TAB_KEY, NUMPAD_0, NUMPAD_1,
33     NUMPAD_2, NUMPAD_3, NUMPAD_4, NUMPAD_5, NUMPAD_6, NUMPAD_7, NUMPAD_8, NUMPAD_9};
34 
35 static const int keystrokeCount = sizeof(keystrokeTable) / sizeof(*keystrokeTable);
36 
37 enum recordingSeekModes {
38     RECORDING_SEEK_MODE_TURN,
39     RECORDING_SEEK_MODE_DEPTH
40 };
41 
recordChar(unsigned char c)42 void recordChar(unsigned char c) {
43     inputRecordBuffer[locationInRecordingBuffer++] = c;
44     recordingLocation++;
45 }
46 
considerFlushingBufferToFile()47 void considerFlushingBufferToFile() {
48     if (locationInRecordingBuffer >= INPUT_RECORD_BUFFER) {
49         flushBufferToFile();
50     }
51 }
52 
53 // compresses a int into a char, discarding stuff we don't need
compressKeystroke(long c)54 unsigned char compressKeystroke(long c) {
55     short i;
56 
57     for (i = 0; i < keystrokeCount; i++) {
58         if (keystrokeTable[i] == c) {
59             return (unsigned char) (128 + i);
60         }
61     }
62     if (c < 256) {
63         return (unsigned char) c;
64     }
65     return UNKNOWN_KEY;
66 }
67 
numberToString(uint64_t number,short numberOfBytes,unsigned char * recordTo)68 void numberToString(uint64_t number, short numberOfBytes, unsigned char *recordTo) {
69     short i;
70     uint64_t n;
71 
72     n = number;
73     for (i=numberOfBytes - 1; i >= 0; i--) {
74         recordTo[i] = n % 256;
75         n /= 256;
76     }
77     brogueAssert(n == 0);
78 }
79 
80 // numberOfBytes can't be greater than 10
recordNumber(unsigned long number,short numberOfBytes)81 void recordNumber(unsigned long number, short numberOfBytes) {
82     short i;
83     unsigned char c[10];
84 
85     numberToString(number, numberOfBytes, c);
86     for (i=0; i<numberOfBytes; i++) {
87         recordChar(c[i]);
88     }
89 }
90 
91 // Events are recorded as follows:
92 // Keystrokes: Event type, keystroke value, modifier flags. (3 bytes.)
93 // All other events: Event type, x-coordinate of the event, y-coordinate of the event, modifier flags. (4 bytes.)
94 // Note: these must be sanitized, because user input may contain more than one byte per parameter.
recordEvent(rogueEvent * event)95 void recordEvent(rogueEvent *event) {
96     unsigned char c;
97 
98     if (rogue.playbackMode) {
99         return;
100     }
101 
102     recordChar((unsigned char) event->eventType);
103 
104     if (event->eventType == KEYSTROKE) {
105         // record which key
106         c = compressKeystroke(event->param1);
107         if (c == UNKNOWN_KEY) {
108             return;
109         }
110         recordChar(c);
111     } else {
112         recordChar((unsigned char) event->param1);
113         recordChar((unsigned char) event->param2);
114     }
115 
116     // record the modifier keys
117     c = 0;
118     if (event->controlKey) {
119         c += Fl(1);
120     }
121     if (event->shiftKey) {
122         c += Fl(2);
123     }
124     recordChar(c);
125 }
126 
127 // For convenience.
recordKeystroke(int keystroke,boolean controlKey,boolean shiftKey)128 void recordKeystroke(int keystroke, boolean controlKey, boolean shiftKey) {
129     rogueEvent theEvent;
130 
131     if (rogue.playbackMode) {
132         return;
133     }
134 
135     theEvent.eventType = KEYSTROKE;
136     theEvent.param1 = keystroke;
137     theEvent.controlKey = controlKey;
138     theEvent.shiftKey = shiftKey;
139     recordEvent(&theEvent);
140 }
141 
cancelKeystroke()142 void cancelKeystroke() {
143     brogueAssert(locationInRecordingBuffer >= 3);
144     locationInRecordingBuffer -= 3; // a keystroke is encoded into 3 bytes
145     recordingLocation -= 3;
146 }
147 
148 // record a series of keystrokes; string must end with a null terminator
recordKeystrokeSequence(unsigned char * keystrokeSequence)149 void recordKeystrokeSequence(unsigned char *keystrokeSequence) {
150     short i;
151     for (i=0; keystrokeSequence[i] != '\0'; i++) {
152         recordKeystroke(keystrokeSequence[i], false, false);
153     }
154 }
155 
156 // For convenience.
recordMouseClick(short x,short y,boolean controlKey,boolean shiftKey)157 void recordMouseClick(short x, short y, boolean controlKey, boolean shiftKey) {
158     rogueEvent theEvent;
159 
160     if (rogue.playbackMode) {
161         return;
162     }
163 
164     theEvent.eventType = MOUSE_UP;
165     theEvent.param1 = x;
166     theEvent.param2 = y;
167     theEvent.controlKey = controlKey;
168     theEvent.shiftKey = shiftKey;
169     recordEvent(&theEvent);
170 }
171 
writeHeaderInfo(char * path)172 void writeHeaderInfo(char *path) {
173     unsigned char c[RECORDING_HEADER_LENGTH];
174     short i;
175     FILE *recordFile;
176 
177     // Zero out the entire header to start.
178     for (i=0; i<RECORDING_HEADER_LENGTH; i++) {
179         c[i] = 0;
180     }
181 
182     // Note the version string to gracefully deny compatibility when necessary.
183     for (i = 0; rogue.versionString[i] != '\0'; i++) {
184         c[i] = rogue.versionString[i];
185     }
186     c[15] = rogue.wizard;
187     i = 16;
188     numberToString(rogue.seed, 8, &c[i]);
189     i += 8;
190     numberToString(rogue.playerTurnNumber, 4, &c[i]);
191     i += 4;
192     numberToString(rogue.deepestLevel, 4, &c[i]);
193     i += 4;
194     numberToString(lengthOfPlaybackFile, 4, &c[i]);
195     i += 4;
196 
197     if (!fileExists(path)) {
198         recordFile = fopen(path, "wb");
199         if (recordFile) {
200             fclose(recordFile);
201         }
202     }
203 
204     recordFile = fopen(path, "r+b");
205     rewind(recordFile);
206     for (i=0; i<RECORDING_HEADER_LENGTH; i++) {
207         putc(c[i], recordFile);
208     }
209     if (recordFile) {
210         fclose(recordFile);
211     }
212 
213     if (lengthOfPlaybackFile < RECORDING_HEADER_LENGTH) {
214         lengthOfPlaybackFile = RECORDING_HEADER_LENGTH;
215     }
216 }
217 
flushBufferToFile()218 void flushBufferToFile() {
219     short i;
220     FILE *recordFile;
221 
222     if (rogue.playbackMode) {
223         return;
224     }
225 
226     lengthOfPlaybackFile += locationInRecordingBuffer;
227     writeHeaderInfo(currentFilePath);
228 
229     if (locationInRecordingBuffer != 0) {
230 
231         recordFile = fopen(currentFilePath, "ab");
232 
233         for (i=0; i<locationInRecordingBuffer; i++) {
234             putc(inputRecordBuffer[i], recordFile);
235         }
236 
237         if (recordFile) {
238             fclose(recordFile);
239         }
240 
241         locationInRecordingBuffer = 0;
242     }
243 }
244 
fillBufferFromFile()245 void fillBufferFromFile() {
246 //  short i;
247     FILE *recordFile;
248 
249     recordFile = fopen(currentFilePath, "rb");
250     fseek(recordFile, positionInPlaybackFile, SEEK_SET);
251 
252     fread((void *) inputRecordBuffer, 1, INPUT_RECORD_BUFFER, recordFile);
253 
254     positionInPlaybackFile = ftell(recordFile);
255     fclose(recordFile);
256 
257     locationInRecordingBuffer = 0;
258 }
259 
recallChar()260 unsigned char recallChar() {
261     unsigned char c;
262     if (recordingLocation > lengthOfPlaybackFile) {
263         return END_OF_RECORDING;
264     }
265     c = inputRecordBuffer[locationInRecordingBuffer++];
266     recordingLocation++;
267     if (locationInRecordingBuffer >= INPUT_RECORD_BUFFER) {
268         fillBufferFromFile();
269     }
270     return c;
271 }
272 
uncompressKeystroke(unsigned char c)273 long uncompressKeystroke(unsigned char c) {
274     if (c >= 128 && (c - 128) < keystrokeCount) {
275         return keystrokeTable[c - 128];
276     }
277     return (long)c;
278 }
279 
recallNumber(short numberOfBytes)280 uint64_t recallNumber(short numberOfBytes) {
281     short i;
282     uint64_t n;
283 
284     n = 0;
285 
286     for (i=0; i<numberOfBytes; i++) {
287         n *= 256;
288         n += (uint64_t) recallChar();
289     }
290     return n;
291 }
292 
293 #define OOS_APOLOGY "Playback of the recording has diverged from the originally recorded game.\n\n\
294 This could be caused by recording or playing the file on a modified version of Brogue, or it could \
295 simply be the result of a bug.\n\n\
296 If this is a different computer from the one on which the recording was saved, the recording \
297 might succeed on the original computer."
298 
playbackPanic()299 void playbackPanic() {
300     cellDisplayBuffer rbuf[COLS][ROWS];
301 
302     if (!rogue.playbackOOS) {
303         rogue.playbackFastForward = false;
304         rogue.playbackPaused = true;
305         rogue.playbackOOS = true;
306         rogue.creaturesWillFlashThisTurn = false;
307         blackOutScreen();
308         displayLevel();
309         refreshSideBar(-1, -1, false);
310 
311         confirmMessages();
312         message("Playback is out of sync.", 0);
313 
314         printTextBox(OOS_APOLOGY, 0, 0, 0, &white, &black, rbuf, NULL, 0);
315 
316         rogue.playbackMode = false;
317         displayMoreSign();
318         rogue.playbackMode = true;
319 
320         overlayDisplayBuffer(rbuf, 0);
321 
322         printf("Playback panic at location %li! Turn number %li.\n", recordingLocation - 1, rogue.playerTurnNumber);
323         overlayDisplayBuffer(rbuf, 0);
324 
325         mainInputLoop();
326     }
327 }
328 
recallEvent(rogueEvent * event)329 void recallEvent(rogueEvent *event) {
330     unsigned char c;
331     boolean tryAgain;
332 
333     do {
334         tryAgain = false;
335         c = recallChar();
336         event->eventType = c;
337 
338         switch (c) {
339             case KEYSTROKE:
340                 // record which key
341                 event->param1 = uncompressKeystroke(recallChar());
342                 event->param2 = 0;
343                 break;
344             case SAVED_GAME_LOADED:
345                 tryAgain = true;
346                 flashTemporaryAlert(" Saved game loaded ", 1000);
347                 break;
348             case MOUSE_UP:
349             case MOUSE_DOWN:
350             case MOUSE_ENTERED_CELL:
351             case RIGHT_MOUSE_UP:
352             case RIGHT_MOUSE_DOWN:
353                 event->param1 = recallChar();
354                 event->param2 = recallChar();
355                 break;
356             case RNG_CHECK:
357             case END_OF_RECORDING:
358             case EVENT_ERROR:
359             default:
360                 message("Unrecognized event type in playback.", REQUIRE_ACKNOWLEDGMENT);
361                 printf("Unrecognized event type in playback: event ID %i", c);
362                 tryAgain = true;
363                 playbackPanic();
364                 break;
365         }
366     } while (tryAgain && !rogue.gameHasEnded);
367 
368     // record the modifier keys
369     c = recallChar();
370     event->controlKey = (c & Fl(1)) ? true : false;
371     event->shiftKey =   (c & Fl(2)) ? true : false;
372 }
373 
loadNextAnnotation()374 void loadNextAnnotation() {
375     unsigned long currentReadTurn;
376     short i;
377     FILE *annotationFile;
378 
379     if (rogue.nextAnnotationTurn == -1) {
380         return;
381     }
382 
383     annotationFile =  fopen(annotationPathname, "r");
384     fseek(annotationFile, rogue.locationInAnnotationFile, SEEK_SET);
385 
386     for (;;) {
387 
388         // load turn number
389         if (fscanf(annotationFile, "%lu\t", &(currentReadTurn)) != 1) {
390             if (feof(annotationFile)) {
391                 rogue.nextAnnotation[0] = '\0';
392                 rogue.nextAnnotationTurn = -1;
393                 break;
394             } else {
395                 // advance to the end of the line
396                 fgets(rogue.nextAnnotation, 5000, annotationFile);
397                 continue;
398             }
399         }
400 
401         // load description
402         fgets(rogue.nextAnnotation, 5000, annotationFile);
403 
404         if (currentReadTurn > rogue.playerTurnNumber ||
405             (currentReadTurn <= 1 && rogue.playerTurnNumber <= 1 && currentReadTurn >= rogue.playerTurnNumber)) {
406             rogue.nextAnnotationTurn = currentReadTurn;
407 
408             // strip the newline off the end
409             rogue.nextAnnotation[strlen(rogue.nextAnnotation) - 1] = '\0';
410             // strip out any gremlins in the annotation
411             for (i=0; i<5000 && rogue.nextAnnotation[i]; i++) {
412                 if (rogue.nextAnnotation[i] < ' '
413                     || rogue.nextAnnotation[i] > '~') {
414                     rogue.nextAnnotation[i] = ' ';
415                 }
416             }
417             break;
418         }
419     }
420     rogue.locationInAnnotationFile = ftell(annotationFile);
421     fclose(annotationFile);
422 }
423 
displayAnnotation()424 void displayAnnotation() {
425     cellDisplayBuffer rbuf[COLS][ROWS];
426 
427     if (rogue.playbackMode
428         && rogue.playerTurnNumber == rogue.nextAnnotationTurn) {
429 
430         if (!rogue.playbackFastForward) {
431             refreshSideBar(-1, -1, false);
432 
433             printTextBox(rogue.nextAnnotation, player.xLoc, 0, 0, &black, &white, rbuf, NULL, 0);
434 
435             rogue.playbackMode = false;
436             displayMoreSign();
437             rogue.playbackMode = true;
438 
439             overlayDisplayBuffer(rbuf, 0);
440         }
441 
442         loadNextAnnotation();
443     }
444 }
445 
446 // Attempts to extract the patch version of versionString into patchVersion,
447 // according to the global pattern. The Major and Minor versions must match ours.
448 // Returns true if successful.
getPatchVersion(char * versionString,unsigned short * patchVersion)449 static boolean getPatchVersion(char *versionString, unsigned short *patchVersion) {
450     if (strcmp(versionString, "CE 1.9") == 0) {
451         // this older version string didn't show the patch number
452         *patchVersion = 0;
453         return BROGUE_MAJOR == 1 && BROGUE_MINOR == 9;
454     }
455     return sscanf(versionString, BROGUE_PATCH_VERSION_PATTERN, patchVersion) == 1;
456 }
457 
458 // creates a game recording file, or if in playback mode,
459 // initializes based on and starts reading from the recording file
initRecording()460 void initRecording() {
461     short i;
462     boolean wizardMode;
463     unsigned short recPatch;
464     char buf[100], *versionString = rogue.versionString;
465     FILE *recordFile;
466 
467 #ifdef AUDIT_RNG
468     if (fileExists(RNG_LOG)) {
469         remove(RNG_LOG);
470     }
471     RNGLogFile = fopen(RNG_LOG, "a");
472 #endif
473 
474     locationInRecordingBuffer   = 0;
475     positionInPlaybackFile      = 0;
476     recordingLocation           = 0;
477     maxLevelChanges             = 0;
478     rogue.playbackOOS           = false;
479     rogue.playbackOmniscience   = false;
480     rogue.nextAnnotationTurn    = 0;
481     rogue.nextAnnotation[0]     = '\0';
482     rogue.locationInAnnotationFile  = 0;
483     rogue.patchVersion          = 0;
484 
485     if (rogue.playbackMode) {
486         lengthOfPlaybackFile        = 100000; // so recall functions don't freak out
487         rogue.playbackDelayPerTurn  = DEFAULT_PLAYBACK_DELAY;
488         rogue.playbackDelayThisTurn = rogue.playbackDelayPerTurn;
489         rogue.playbackPaused        = false;
490 
491         fillBufferFromFile();
492 
493         for (i=0; i<15; i++) {
494             versionString[i] = recallChar();
495         }
496         wizardMode = recallChar();
497 
498         if (getPatchVersion(versionString, &recPatch) && recPatch <= BROGUE_PATCH) {
499             // Major and Minor match ours, Patch is less than or equal to ours: we are compatible.
500             rogue.patchVersion = recPatch;
501         } else if (strcmp(versionString, BROGUE_RECORDING_VERSION_STRING) != 0) {
502             // We have neither a compatible pattern match nor an exact match: we cannot load it.
503             rogue.playbackMode = false;
504             rogue.playbackFastForward = false;
505             sprintf(buf, "This file is from version %s and cannot be opened in version %s.", versionString, BROGUE_VERSION_STRING);
506             dialogAlert(buf);
507             rogue.playbackMode = true;
508             rogue.playbackPaused = true;
509             rogue.playbackFastForward = false;
510             rogue.playbackOOS = false;
511             rogue.gameHasEnded = true;
512         }
513 
514         if (wizardMode != rogue.wizard) {
515             // wizard game cannot be played in normal mode and vice versa
516             rogue.playbackMode = false;
517             rogue.playbackFastForward = false;
518             if (wizardMode) {
519                 sprintf(buf, "This game was played in wizard mode. You must start Brogue in wizard mode to replay it.");
520             } else {
521                 sprintf(buf, "To play this regular recording, please restart Brogue without the wizard mode option.");
522             }
523             dialogAlert(buf);
524             rogue.playbackMode = true;
525             rogue.playbackPaused = true;
526             rogue.playbackFastForward = false;
527             rogue.playbackOOS = false;
528             rogue.gameHasEnded = true;
529         }
530 
531         rogue.seed              = recallNumber(8);          // master random seed
532         rogue.howManyTurns      = recallNumber(4);          // how many turns are in this recording
533         maxLevelChanges         = recallNumber(4);          // how many times the player changes depths
534         lengthOfPlaybackFile    = recallNumber(4);
535         seedRandomGenerator(rogue.seed);
536         previousGameSeed = rogue.seed;
537 
538         if (fileExists(annotationPathname)) {
539             loadNextAnnotation();
540         } else {
541             rogue.nextAnnotationTurn = -1;
542         }
543     } else {
544         // If present, set the patch version for playing the game.
545         rogue.patchVersion = BROGUE_PATCH;
546         strcpy(versionString, BROGUE_RECORDING_VERSION_STRING);
547 
548         lengthOfPlaybackFile = 1;
549         remove(currentFilePath);
550         recordFile = fopen(currentFilePath, "wb"); // create the file
551         fclose(recordFile);
552 
553         flushBufferToFile(); // header info never makes it into inputRecordBuffer when recording
554         rogue.recording = true;
555     }
556     rogue.currentTurnNumber = 0;
557 }
558 
OOSCheck(unsigned long x,short numberOfBytes)559 void OOSCheck(unsigned long x, short numberOfBytes) {
560     unsigned char eventType;
561     unsigned long recordedNumber;
562 
563     if (rogue.playbackMode) {
564         eventType = recallChar();
565         recordedNumber = recallNumber(numberOfBytes);
566         if (eventType != RNG_CHECK || recordedNumber != x) {
567             if (eventType != RNG_CHECK) {
568                 printf("Event type mismatch in RNG check.\n");
569                 playbackPanic();
570             } else if (recordedNumber != x) {
571                 printf("Expected RNG output of %li; got %i.\n", recordedNumber, (int) x);
572                 playbackPanic();
573             }
574         }
575     } else {
576         recordChar(RNG_CHECK);
577         recordNumber(x, numberOfBytes);
578         considerFlushingBufferToFile();
579     }
580 }
581 
582 // compare a random number once per player turn so we instantly know if we are out of sync during playback
RNGCheck()583 void RNGCheck() {
584     short oldRNG;
585     unsigned long randomNumber;
586 
587     oldRNG = rogue.RNG;
588     rogue.RNG = RNG_SUBSTANTIVE;
589 
590 //#ifdef AUDIT_RNG
591 //reportRNGState();
592 //#endif
593 
594     randomNumber = (unsigned long) rand_range(0, 255);
595     OOSCheck(randomNumber, 1);
596 
597     rogue.RNG = oldRNG;
598 }
599 
unpause()600 boolean unpause() {
601     if (rogue.playbackOOS) {
602         flashTemporaryAlert(" Out of sync ", 2000);
603     } else if (rogue.playbackPaused) {
604         rogue.playbackPaused = false;
605         return true;
606     }
607     return false;
608 }
609 
610 #define PLAYBACK_HELP_LINE_COUNT    20
611 
printPlaybackHelpScreen()612 void printPlaybackHelpScreen() {
613     short i, j;
614     cellDisplayBuffer dbuf[COLS][ROWS], rbuf[COLS][ROWS];
615     char helpText[PLAYBACK_HELP_LINE_COUNT][80] = {
616         "Commands:",
617         "",
618         "         <space>: ****pause or unpause playback",
619         "   k or up arrow: ****play back faster",
620         " j or down arrow: ****play back slower",
621         "               <: ****go to previous level",
622         "               >: ****go to next level",
623         "             0-9: ****skip to specified turn number",
624         "l or right arrow: ****advance one turn (shift for 5 turns; control for 20)",
625         "",
626         "           <tab>: ****enable or disable omniscience",
627         "          return: ****examine surroundings",
628         "               i: ****display inventory",
629         "               D: ****display discovered items",
630         "               V: ****view saved recording",
631         "               O: ****open and resume saved game",
632         "               N: ****begin a new game",
633         "               Q: ****quit to title screen",
634         "",
635         "        -- press any key to continue --"
636     };
637 
638     // Replace the "****"s with color escapes.
639     for (i=0; i<PLAYBACK_HELP_LINE_COUNT; i++) {
640         for (j=0; helpText[i][j]; j++) {
641             if (helpText[i][j] == '*') {
642                 j = encodeMessageColor(helpText[i], j, &white);
643             }
644         }
645     }
646 
647     clearDisplayBuffer(dbuf);
648 
649     for (i=0; i<PLAYBACK_HELP_LINE_COUNT; i++) {
650         printString(helpText[i], mapToWindowX(5), mapToWindowY(i), &itemMessageColor, &black, dbuf);
651     }
652 
653     for (i=0; i<COLS; i++) {
654         for (j=0; j<ROWS; j++) {
655             dbuf[i][j].opacity = (i < STAT_BAR_WIDTH ? 0 : INTERFACE_OPACITY);
656         }
657     }
658     overlayDisplayBuffer(dbuf, rbuf);
659 
660     rogue.playbackMode = false;
661     waitForAcknowledgment();
662     rogue.playbackMode = true;
663     overlayDisplayBuffer(rbuf, NULL);
664 }
665 
resetPlayback()666 static void resetPlayback() {
667     boolean omniscient, stealth, trueColors;
668 
669     omniscient = rogue.playbackOmniscience;
670     stealth = rogue.displayAggroRangeMode;
671     trueColors = rogue.trueColorMode;
672 
673     freeEverything();
674     randomNumbersGenerated = 0;
675     rogue.playbackMode = true;
676     initializeRogue(0); // Seed argument is ignored because we're in playback.
677 
678     rogue.playbackOmniscience = omniscient;
679     rogue.displayAggroRangeMode = stealth;
680     rogue.trueColorMode = trueColors;
681 
682     rogue.playbackFastForward = false;
683     blackOutScreen();
684     rogue.playbackFastForward = true;
685     startLevel(rogue.depthLevel, 1);
686 }
687 
seek(unsigned long seekTarget,enum recordingSeekModes seekMode)688 static void seek(unsigned long seekTarget, enum recordingSeekModes seekMode) {
689     unsigned long progressBarRefreshInterval = 1, startTurnNumber = 0, targetTurnNumber = 0, avgTurnsPerLevel = 1;
690     rogueEvent theEvent;
691     boolean pauseState, useProgressBar = false, arrivedAtDestination = false;
692     cellDisplayBuffer dbuf[COLS][ROWS];
693 
694     pauseState = rogue.playbackPaused;
695 
696     // configure progress bar
697     switch (seekMode) {
698         case RECORDING_SEEK_MODE_DEPTH :
699             if (maxLevelChanges > 0) {
700                 avgTurnsPerLevel = rogue.howManyTurns / maxLevelChanges;
701             }
702             if (seekTarget <= rogue.depthLevel) {
703                 startTurnNumber = 0;
704                 targetTurnNumber = avgTurnsPerLevel * seekTarget;
705             } else {
706                 startTurnNumber = rogue.playerTurnNumber;
707                 targetTurnNumber = rogue.playerTurnNumber + avgTurnsPerLevel;
708             }
709             break;
710         case RECORDING_SEEK_MODE_TURN :
711             if (seekTarget < rogue.playerTurnNumber) {
712                 startTurnNumber = 0;
713                 targetTurnNumber = seekTarget;
714             } else {
715                 startTurnNumber = rogue.playerTurnNumber;
716                 targetTurnNumber = seekTarget;
717             }
718             break;
719     }
720 
721     if (targetTurnNumber - startTurnNumber > 100) {
722         useProgressBar = true;
723         progressBarRefreshInterval = max(1, (targetTurnNumber) / 500);
724     }
725 
726     // there is no rewind, so start over at depth 1
727     if ((seekMode == RECORDING_SEEK_MODE_TURN && seekTarget < rogue.playerTurnNumber)
728         || (seekMode == RECORDING_SEEK_MODE_DEPTH && seekTarget <= rogue.depthLevel)) {
729 
730         resetPlayback();
731         if ((seekMode == RECORDING_SEEK_MODE_DEPTH && seekTarget == 1)
732             || (seekMode == RECORDING_SEEK_MODE_TURN && seekTarget == 0)) {
733             arrivedAtDestination = true;
734         }
735     }
736 
737     if (useProgressBar) {
738         clearDisplayBuffer(dbuf);
739         rectangularShading((COLS - 20) / 2, ROWS / 2, 20, 1, &black, INTERFACE_OPACITY, dbuf);
740         overlayDisplayBuffer(dbuf, 0);
741         commitDraws();
742     }
743     rogue.playbackFastForward = true;
744 
745     while (!arrivedAtDestination && !rogue.gameHasEnded && !rogue.playbackOOS) {
746         if (useProgressBar && !(rogue.playerTurnNumber % progressBarRefreshInterval)) {
747             rogue.playbackFastForward = false; // so that pauseBrogue looks for inputs
748             printProgressBar((COLS - 20) / 2, ROWS / 2, "[     Loading...   ]",
749                              rogue.playerTurnNumber - startTurnNumber,
750                              targetTurnNumber - startTurnNumber, &darkPurple, false);
751             while (pauseBrogue(0)) { // pauseBrogue(0) is necessary to flush the display to the window in SDL
752                 if (rogue.gameHasEnded) {
753                     return;
754                 }
755                 rogue.creaturesWillFlashThisTurn = false; // prevent monster flashes from showing up on screen
756                 nextBrogueEvent(&theEvent, true, false, true); // eat the input if it isn't clicking x
757             }
758             rogue.playbackFastForward = true;
759         }
760 
761         rogue.RNG = RNG_COSMETIC; // dancing terrain colors can't influence recordings
762         rogue.playbackDelayThisTurn = 0;
763         nextBrogueEvent(&theEvent, false, true, false);
764         rogue.RNG = RNG_SUBSTANTIVE;
765         executeEvent(&theEvent);
766 
767         if ((seekMode == RECORDING_SEEK_MODE_DEPTH && rogue.depthLevel == seekTarget)
768             || (seekMode == RECORDING_SEEK_MODE_TURN && rogue.playerTurnNumber == seekTarget)) {
769 
770             arrivedAtDestination = true;
771         }
772     }
773 
774     rogue.playbackPaused = pauseState;
775     rogue.playbackFastForward = false;
776     confirmMessages();
777     updateMessageDisplay();
778     refreshSideBar(-1, -1, false);
779     displayLevel();
780 }
781 
promptToAdvanceToLocation(short keystroke)782 void promptToAdvanceToLocation(short keystroke) {
783     char entryText[30], buf[max(30, DCOLS)];
784     unsigned long destinationFrame;
785     boolean enteredText;
786 
787     if (rogue.playbackOOS || !rogue.playbackPaused || unpause()) {
788         buf[0] = (keystroke == '0' ? '\0' : keystroke);
789         buf[1] = '\0';
790 
791         rogue.playbackMode = false;
792         enteredText = getInputTextString(entryText, "Go to turn number: ", 9, buf, "", TEXT_INPUT_NUMBERS, false);
793         confirmMessages();
794         rogue.playbackMode = true;
795 
796         if (enteredText && entryText[0] != '\0') {
797             sscanf(entryText, "%lu", &destinationFrame);
798 
799             if (rogue.playbackOOS && destinationFrame > rogue.playerTurnNumber) {
800                 flashTemporaryAlert(" Out of sync ", 3000);
801             } else if (destinationFrame == rogue.playerTurnNumber) {
802                 sprintf(buf, " Already at turn %li ", destinationFrame);
803                 flashTemporaryAlert(buf, 1000);
804             } else {
805                 seek(min(destinationFrame, rogue.howManyTurns), RECORDING_SEEK_MODE_TURN);
806             }
807             rogue.playbackPaused = true;
808         }
809     }
810 }
811 
pausePlayback()812 void pausePlayback() {
813     //short oldRNG;
814     if (!rogue.playbackPaused) {
815         rogue.playbackPaused = true;
816         messageWithColor(KEYBOARD_LABELS ? "recording paused. Press space to play." : "recording paused.",
817                          &teal, 0);
818         refreshSideBar(-1, -1, false);
819         //oldRNG = rogue.RNG;
820         //rogue.RNG = RNG_SUBSTANTIVE;
821         mainInputLoop();
822         //rogue.RNG = oldRNG;
823         messageWithColor("recording unpaused.", &teal, 0);
824         rogue.playbackPaused = false;
825         refreshSideBar(-1, -1, false);
826         rogue.playbackDelayThisTurn = DEFAULT_PLAYBACK_DELAY;
827     }
828 }
829 
830 // Used to interact with playback -- e.g. changing speed, pausing.
executePlaybackInput(rogueEvent * recordingInput)831 boolean executePlaybackInput(rogueEvent *recordingInput) {
832     signed long key;
833     short newDelay, frameCount, x, y;
834     unsigned long destinationFrame;
835     boolean proceed;
836     char path[BROGUE_FILENAME_MAX];
837 
838     if (!rogue.playbackMode) {
839         return false;
840     }
841 
842     if (recordingInput->eventType == KEYSTROKE) {
843         key = recordingInput->param1;
844         stripShiftFromMovementKeystroke(&key);
845 
846         switch (key) {
847             case UP_ARROW:
848             case UP_KEY:
849                 newDelay = max(1, min(rogue.playbackDelayPerTurn * 2/3, rogue.playbackDelayPerTurn - 1));
850                 if (newDelay != rogue.playbackDelayPerTurn) {
851                     flashTemporaryAlert(" Faster ", 300);
852                 }
853                 rogue.playbackDelayPerTurn = newDelay;
854                 rogue.playbackDelayThisTurn = rogue.playbackDelayPerTurn;
855                 return true;
856             case DOWN_ARROW:
857             case DOWN_KEY:
858                 newDelay = min(3000, max(rogue.playbackDelayPerTurn * 3/2, rogue.playbackDelayPerTurn + 1));
859                 if (newDelay != rogue.playbackDelayPerTurn) {
860                     flashTemporaryAlert(" Slower ", 300);
861                 }
862                 rogue.playbackDelayPerTurn = newDelay;
863                 rogue.playbackDelayThisTurn = rogue.playbackDelayPerTurn;
864                 return true;
865             case ACKNOWLEDGE_KEY:
866                 if (rogue.playbackOOS && rogue.playbackPaused) {
867                     flashTemporaryAlert(" Out of sync ", 2000);
868                 } else {
869                     rogue.playbackPaused = !rogue.playbackPaused;
870                 }
871                 return true;
872             case TAB_KEY:
873                 rogue.playbackOmniscience = !rogue.playbackOmniscience;
874                 displayLevel();
875                 refreshSideBar(-1, -1, false);
876                 if (rogue.playbackOmniscience) {
877                     messageWithColor("Omniscience enabled.", &teal, 0);
878                 } else {
879                     messageWithColor("Omniscience disabled.", &teal, 0);
880                 }
881                 return true;
882             case ASCEND_KEY:
883                 seek(max(rogue.depthLevel - 1, 1), RECORDING_SEEK_MODE_DEPTH);
884                 return true;
885             case DESCEND_KEY:
886                 if (rogue.depthLevel == maxLevelChanges) {
887                     flashTemporaryAlert(" Already reached deepest depth explored ", 2000);
888                     return false;
889                 }
890                 seek(rogue.depthLevel + 1, RECORDING_SEEK_MODE_DEPTH);
891                 return true;
892             case INVENTORY_KEY:
893                 rogue.playbackMode = false;
894                 displayInventory(ALL_ITEMS, 0, 0, true, false);
895                 rogue.playbackMode = true;
896                 return true;
897             case RIGHT_KEY:
898             case RIGHT_ARROW:
899             case LEFT_KEY:
900             case LEFT_ARROW:
901                 if (key == RIGHT_KEY || key == RIGHT_ARROW) {
902                     frameCount = 1;
903                 } else {
904                     frameCount = -1;
905                 }
906                 if (recordingInput->shiftKey) {
907                     frameCount *= 5;
908                 }
909                 if (recordingInput->controlKey) {
910                     frameCount *= 20;
911                 }
912 
913                 if (frameCount < 0) {
914                     if ((unsigned long) (frameCount * -1) > rogue.playerTurnNumber) {
915                         destinationFrame = 0;
916                     } else {
917                         destinationFrame = rogue.playerTurnNumber + frameCount;
918                     }
919                 } else {
920                     destinationFrame = min(rogue.playerTurnNumber + frameCount, rogue.howManyTurns);
921                 }
922 
923                 if (destinationFrame == rogue.playerTurnNumber) {
924                     flashTemporaryAlert(" Already at end of recording ", 1000);
925                 } else if (frameCount < 0) {
926                     rogue.playbackMode = false;
927                     proceed = (rogue.playerTurnNumber < 100 || confirm("Rewind?", true));
928                     rogue.playbackMode = true;
929                     if (proceed) {
930                         seek(destinationFrame, RECORDING_SEEK_MODE_TURN);
931                     }
932                 } else {
933                     // advance by the right number of turns
934                     seek(destinationFrame, RECORDING_SEEK_MODE_TURN);
935                 }
936                 return true;
937             case BROGUE_HELP_KEY:
938                 printPlaybackHelpScreen();
939                 return true;
940             case DISCOVERIES_KEY:
941                 rogue.playbackMode = false;
942                 printDiscoveriesScreen();
943                 rogue.playbackMode = true;
944                 return true;
945             case MESSAGE_ARCHIVE_KEY:
946                 rogue.playbackMode = false;
947                 displayMessageArchive();
948                 rogue.playbackMode = true;
949                 return true;
950             case VIEW_RECORDING_KEY:
951                 confirmMessages();
952                 rogue.playbackMode = false;
953                 if (dialogChooseFile(path, RECORDING_SUFFIX, "View recording: ")) {
954                     if (fileExists(path)) {
955                         strcpy(rogue.nextGamePath, path);
956                         rogue.nextGame = NG_VIEW_RECORDING;
957                         rogue.gameHasEnded = true;
958                     } else {
959                         message("File not found.", 0);
960                     }
961                 }
962                 rogue.playbackMode = true;
963                 return true;
964             case LOAD_SAVED_GAME_KEY:
965                 confirmMessages();
966                 rogue.playbackMode = false;
967                 if (dialogChooseFile(path, GAME_SUFFIX, "Open saved game: ")) {
968                     if (fileExists(path)) {
969                         strcpy(rogue.nextGamePath, path);
970                         rogue.nextGame = NG_OPEN_GAME;
971                         rogue.gameHasEnded = true;
972                     } else {
973                         message("File not found.", 0);
974                     }
975                 }
976                 rogue.playbackMode = true;
977                 return true;
978             case NEW_GAME_KEY:
979                 rogue.playbackMode = false;
980                 if (confirm("Close recording and begin a new game?", true)) {
981                     rogue.nextGame = NG_NEW_GAME;
982                     rogue.gameHasEnded = true;
983                 }
984                 rogue.playbackMode = true;
985                 return true;
986             case QUIT_KEY:
987                 //freeEverything();
988                 rogue.gameHasEnded = true;
989                 rogue.playbackOOS = false;
990                 rogue.creaturesWillFlashThisTurn = false;
991                 notifyEvent(GAMEOVER_RECORDING, 0, 0, "recording ended", "none");
992                 return true;
993             case TRUE_COLORS_KEY:
994                 rogue.trueColorMode = !rogue.trueColorMode;
995                 displayLevel();
996                 refreshSideBar(-1, -1, false);
997                 if (rogue.trueColorMode) {
998                     messageWithColor(KEYBOARD_LABELS ? "Color effects disabled. Press '\\' again to enable." : "Color effects disabled.",
999                                      &teal, 0);
1000                 } else {
1001                     messageWithColor(KEYBOARD_LABELS ? "Color effects enabled. Press '\\' again to disable." : "Color effects enabled.",
1002                                      &teal, 0);
1003                 }
1004                 return true;
1005             case AGGRO_DISPLAY_KEY:
1006                 rogue.displayAggroRangeMode = !rogue.displayAggroRangeMode;
1007                 displayLevel();
1008                 refreshSideBar(-1, -1, false);
1009                 if (rogue.displayAggroRangeMode) {
1010                     messageWithColor(KEYBOARD_LABELS ? "Stealth range displayed. Press ']' again to hide." : "Stealth range displayed.",
1011                                      &teal, 0);
1012                 } else {
1013                     messageWithColor(KEYBOARD_LABELS ? "Stealth range hidden. Press ']' again to display." : "Stealth range hidden.",
1014                                      &teal, 0);
1015                 }
1016                 return true;
1017             case GRAPHICS_KEY:
1018                 if (hasGraphics) {
1019                     graphicsMode = setGraphicsMode((graphicsMode + 1) % 3);
1020                     switch (graphicsMode) {
1021                         case TEXT_GRAPHICS:
1022                             messageWithColor(KEYBOARD_LABELS
1023                                 ? "Switched to text mode. Press 'G' again to enable tiles."
1024                                 : "Switched to text mode.", &teal, 0);
1025                             break;
1026                         case TILES_GRAPHICS:
1027                             messageWithColor(KEYBOARD_LABELS
1028                                 ? "Switched to graphical tiles. Press 'G' again to enable hybrid mode."
1029                                 : "Switched to graphical tiles.", &teal, 0);
1030                             break;
1031                         case HYBRID_GRAPHICS:
1032                             messageWithColor(KEYBOARD_LABELS
1033                                 ? "Switched to hybrid mode. Press 'G' again to disable tiles."
1034                                 : "Switched to hybrid mode.", &teal, 0);
1035                             break;
1036                     }
1037                 }
1038                 return true;
1039             case SEED_KEY:
1040                 //rogue.playbackMode = false;
1041                 //DEBUG {displayGrid(safetyMap); displayMoreSign(); displayLevel();}
1042                 //rogue.playbackMode = true;
1043                 printSeed();
1044                 return true;
1045             case SWITCH_TO_PLAYING_KEY:
1046 #ifdef ENABLE_PLAYBACK_SWITCH
1047                     if (!rogue.gameHasEnded && !rogue.playbackOOS) {
1048                         switchToPlaying();
1049                         lengthOfPlaybackFile = recordingLocation;
1050                     }
1051                     return true;
1052 #endif
1053                 return false;
1054             case ESCAPE_KEY:
1055                 if (!rogue.playbackPaused) {
1056                     rogue.playbackPaused = true;
1057                     return true;
1058                 }
1059                 return false;
1060             default:
1061                 if (key >= '0' && key <= '9'
1062                     || key >= NUMPAD_0 && key <= NUMPAD_9) {
1063 
1064                     promptToAdvanceToLocation(key);
1065                     return true;
1066                 }
1067                 return false;
1068         }
1069     } else if (recordingInput->eventType == MOUSE_UP) {
1070         x = recordingInput->param1;
1071         y = recordingInput->param2;
1072         if (windowToMapX(x) >= 0 && windowToMapX(x) < DCOLS && y >= 0 && y < MESSAGE_LINES) {
1073             // If the click location is in the message block, display the message archive.
1074             rogue.playbackMode = false;
1075             displayMessageArchive();
1076             rogue.playbackMode = true;
1077             return true;
1078         } else if (!rogue.playbackPaused) {
1079             // clicking anywhere else pauses the playback
1080             rogue.playbackPaused = true;
1081             return true;
1082         }
1083     } else if (recordingInput->eventType == RIGHT_MOUSE_UP) {
1084         rogue.playbackMode = false;
1085         displayInventory(ALL_ITEMS, 0, 0, true, false);
1086         rogue.playbackMode = true;
1087         return true;
1088     }
1089     return false;
1090 }
1091 
1092 // Pass in defaultPath (the file name WITHOUT suffix), and the suffix.
1093 // Get back either defaultPath, or "defaultPath N",
1094 // where N is the lowest counting number that doesn't collide with an existing file.
getAvailableFilePath(char * returnPath,const char * defaultPath,const char * suffix)1095 void getAvailableFilePath(char *returnPath, const char *defaultPath, const char *suffix) {
1096     char fullPath[BROGUE_FILENAME_MAX];
1097     short fileNameIterator = 2;
1098 
1099     strcpy(returnPath, defaultPath);
1100     sprintf(fullPath, "%s%s", returnPath, suffix);
1101     while (fileExists(fullPath)) {
1102         sprintf(returnPath, "%s (%i)", defaultPath, fileNameIterator);
1103         sprintf(fullPath, "%s%s", returnPath, suffix);
1104         fileNameIterator++;
1105     }
1106 }
1107 
characterForbiddenInFilename(const char theChar)1108 boolean characterForbiddenInFilename(const char theChar) {
1109     if (theChar == '/' || theChar == '\\' || theChar == ':') {
1110         return true;
1111     } else {
1112         return false;
1113     }
1114 }
1115 
getDefaultFilePath(char * defaultPath,boolean gameOver)1116 void getDefaultFilePath(char *defaultPath, boolean gameOver) {
1117     char seed[21];
1118 
1119     // 32-bit numbers are printed in full
1120     // 64-bit numbers longer than 11 digits are shortened to e.g "184...51615"
1121     sprintf(seed, "%llu", (unsigned long long)rogue.seed);
1122     if (strlen(seed) > 11) sprintf(seed+3, "...%05lu", (unsigned long)(rogue.seed % 100000));
1123 
1124     if (serverMode) {
1125         // With WebBrogue, filenames must fit into 30 bytes, including extension and terminal \0.
1126         // It is enough for the seed and the optional file counter. The longest filename will be:
1127         // "#184...51615 (999).broguesave" (30 bytes)
1128         sprintf(defaultPath, "#%s", seed);
1129         return;
1130     }
1131 
1132     if (!gameOver) {
1133         sprintf(defaultPath, "Saved #%s at depth %d", seed, rogue.depthLevel);
1134     } else if (rogue.quit) {
1135         sprintf(defaultPath, "#%s Quit at depth %d", seed, rogue.depthLevel);
1136     } else if (player.bookkeepingFlags & MB_IS_DYING) {
1137         sprintf(defaultPath, "#%s Died at depth %d", seed, rogue.depthLevel);
1138     } else if (rogue.depthLevel > 26) {
1139         sprintf(defaultPath, "#%s Mastered the dungeons", seed);
1140     } else {
1141         sprintf(defaultPath, "#%s Escaped the dungeons", seed);
1142     }
1143     if (rogue.wizard) {
1144         strcat(defaultPath, " (wizard)");
1145     } else if (rogue.easyMode) {
1146         strcat(defaultPath, " (easy)");
1147     }
1148 }
1149 
saveGameNoPrompt()1150 void saveGameNoPrompt() {
1151     char filePath[BROGUE_FILENAME_MAX], defaultPath[BROGUE_FILENAME_MAX];
1152     if (rogue.playbackMode) {
1153         return;
1154     }
1155     getDefaultFilePath(defaultPath, false);
1156     getAvailableFilePath(filePath, defaultPath, GAME_SUFFIX);
1157     flushBufferToFile();
1158     strcat(filePath, GAME_SUFFIX);
1159     rename(currentFilePath, filePath);
1160     strcpy(currentFilePath, filePath);
1161     rogue.gameHasEnded = true;
1162     rogue.recording = false;
1163 }
1164 
saveGame()1165 void saveGame() {
1166     char filePathWithoutSuffix[BROGUE_FILENAME_MAX], filePath[BROGUE_FILENAME_MAX], defaultPath[BROGUE_FILENAME_MAX];
1167     boolean askAgain;
1168 
1169     if (rogue.playbackMode) {
1170         return; // Call me paranoid, but I'd rather it be impossible to embed malware in a recording.
1171     }
1172 
1173     getDefaultFilePath(defaultPath, false);
1174     getAvailableFilePath(filePathWithoutSuffix, defaultPath, GAME_SUFFIX);
1175     filePath[0] = '\0';
1176 
1177     deleteMessages();
1178     do {
1179         askAgain = false;
1180         if (getInputTextString(filePathWithoutSuffix, "Save game as (<esc> to cancel): ",
1181                                BROGUE_FILENAME_MAX - strlen(GAME_SUFFIX), filePathWithoutSuffix, GAME_SUFFIX, TEXT_INPUT_FILENAME, false)) {
1182             snprintf(filePath, BROGUE_FILENAME_MAX, "%s%s", filePathWithoutSuffix, GAME_SUFFIX);
1183             if (!fileExists(filePath) || confirm("File of that name already exists. Overwrite?", true)) {
1184                 remove(filePath);
1185                 flushBufferToFile();
1186                 rename(currentFilePath, filePath);
1187                 strcpy(currentFilePath, filePath);
1188                 rogue.recording = false;
1189                 message("Saved.", REQUIRE_ACKNOWLEDGMENT);
1190                 rogue.gameHasEnded = true;
1191             } else {
1192                 askAgain = true;
1193             }
1194         }
1195     } while (askAgain);
1196     displayRecentMessages();
1197 }
1198 
saveRecordingNoPrompt(char * filePath)1199 void saveRecordingNoPrompt(char *filePath) {
1200     char defaultPath[BROGUE_FILENAME_MAX];
1201     if (rogue.playbackMode) {
1202         return;
1203     }
1204     getDefaultFilePath(defaultPath, true);
1205     getAvailableFilePath(filePath, defaultPath, RECORDING_SUFFIX);
1206     strcat(filePath, RECORDING_SUFFIX);
1207     remove(filePath);
1208     rename(currentFilePath, filePath);
1209     rogue.recording = false;
1210 }
1211 
saveRecording(char * filePathWithoutSuffix)1212 void saveRecording(char *filePathWithoutSuffix) {
1213     char filePath[BROGUE_FILENAME_MAX], defaultPath[BROGUE_FILENAME_MAX];
1214     boolean askAgain;
1215 
1216     if (rogue.playbackMode) {
1217         return;
1218     }
1219 
1220     getDefaultFilePath(defaultPath, true);
1221     getAvailableFilePath(filePathWithoutSuffix, defaultPath, RECORDING_SUFFIX);
1222     filePath[0] = '\0';
1223 
1224     deleteMessages();
1225     do {
1226         askAgain = false;
1227         if (getInputTextString(filePathWithoutSuffix, "Save recording as (<esc> to cancel): ",
1228                                BROGUE_FILENAME_MAX - strlen(RECORDING_SUFFIX), filePathWithoutSuffix, RECORDING_SUFFIX, TEXT_INPUT_FILENAME, false)) {
1229 
1230             snprintf(filePath, BROGUE_FILENAME_MAX, "%s%s", filePathWithoutSuffix, RECORDING_SUFFIX);
1231             if (!fileExists(filePath) || confirm("File of that name already exists. Overwrite?", true)) {
1232                 remove(filePath);
1233                 rename(currentFilePath, filePath);
1234                 rogue.recording = false;
1235             } else {
1236                 askAgain = true;
1237             }
1238         } else { // Declined to save recording; save it anyway as LastRecording, and delete LastRecording if it already exists.
1239             snprintf(filePath, BROGUE_FILENAME_MAX, "%s%s", LAST_RECORDING_NAME, RECORDING_SUFFIX);
1240             if (fileExists(filePath)) {
1241                 remove(filePath);
1242             }
1243             rename(currentFilePath, filePath);
1244             rogue.recording = false;
1245         }
1246     } while (askAgain);
1247     deleteMessages();
1248 }
1249 
copyFile(char * fromFilePath,char * toFilePath,unsigned long fromFileLength)1250 void copyFile(char *fromFilePath, char *toFilePath, unsigned long fromFileLength) {
1251     unsigned long m, n;
1252     unsigned char fileBuffer[INPUT_RECORD_BUFFER];
1253     FILE *fromFile, *toFile;
1254 
1255     remove(toFilePath);
1256 
1257     fromFile    = fopen(fromFilePath, "rb");
1258     toFile      = fopen(toFilePath, "wb");
1259 
1260     for (n = 0; n < fromFileLength; n += m) {
1261         m = min(INPUT_RECORD_BUFFER, fromFileLength - n);
1262         fread((void *) fileBuffer, 1, m, fromFile);
1263         fwrite((void *) fileBuffer, 1, m, toFile);
1264     }
1265 
1266     fclose(fromFile);
1267     fclose(toFile);
1268 }
1269 
1270 // at the end of loading a saved game, this function transitions into active play mode.
switchToPlaying()1271 void switchToPlaying() {
1272     char lastGamePath[BROGUE_FILENAME_MAX];
1273 
1274     getAvailableFilePath(lastGamePath, LAST_GAME_NAME, GAME_SUFFIX);
1275     strcat(lastGamePath, GAME_SUFFIX);
1276 
1277     rogue.playbackMode          = false;
1278     rogue.playbackFastForward   = false;
1279     rogue.playbackOmniscience   = false;
1280     rogue.recording             = true;
1281     locationInRecordingBuffer   = 0;
1282     copyFile(currentFilePath, lastGamePath, recordingLocation);
1283 #ifndef ENABLE_PLAYBACK_SWITCH
1284     if (DELETE_SAVE_FILE_AFTER_LOADING) {
1285         remove(currentFilePath);
1286     }
1287 #endif
1288 
1289     strcpy(currentFilePath, lastGamePath);
1290 
1291     blackOutScreen();
1292     refreshSideBar(-1, -1, false);
1293     updateMessageDisplay();
1294     displayLevel();
1295 }
1296 
1297 // Return whether the load was cancelled by an event
loadSavedGame()1298 boolean loadSavedGame() {
1299     unsigned long progressBarInterval;
1300     unsigned long previousRecordingLocation;
1301     rogueEvent theEvent;
1302 
1303     cellDisplayBuffer dbuf[COLS][ROWS];
1304 
1305     randomNumbersGenerated = 0;
1306     rogue.playbackMode = true;
1307     rogue.playbackFastForward = true;
1308     initializeRogue(0); // Calls initRecording(). Seed argument is ignored because we're initially in playback mode.
1309     if (!rogue.gameHasEnded) {
1310         blackOutScreen();
1311         startLevel(rogue.depthLevel, 1);
1312     }
1313 
1314     if (rogue.howManyTurns > 0) {
1315 
1316         progressBarInterval = max(1, lengthOfPlaybackFile / 100);
1317         previousRecordingLocation = -1; // unsigned
1318         clearDisplayBuffer(dbuf);
1319         rectangularShading((COLS - 20) / 2, ROWS / 2, 20, 1, &black, INTERFACE_OPACITY, dbuf);
1320         rogue.playbackFastForward = false;
1321         overlayDisplayBuffer(dbuf, 0);
1322         rogue.playbackFastForward = true;
1323 
1324         while (recordingLocation < lengthOfPlaybackFile
1325                && rogue.playerTurnNumber < rogue.howManyTurns
1326                && !rogue.gameHasEnded
1327                && !rogue.playbackOOS) {
1328 
1329             rogue.RNG = RNG_COSMETIC;
1330             nextBrogueEvent(&theEvent, false, true, false);
1331             rogue.RNG = RNG_SUBSTANTIVE;
1332 
1333             executeEvent(&theEvent);
1334 
1335             if (recordingLocation / progressBarInterval != previousRecordingLocation / progressBarInterval && !rogue.playbackOOS) {
1336                 rogue.playbackFastForward = false; // so that pauseBrogue looks for inputs
1337                 printProgressBar((COLS - 20) / 2, ROWS / 2, "[     Loading...   ]", recordingLocation, lengthOfPlaybackFile, &darkPurple, false);
1338                 while (pauseBrogue(0)) { // pauseBrogue(0) is necessary to flush the display to the window in SDL, as well as look for inputs
1339                     rogue.creaturesWillFlashThisTurn = false; // prevent monster flashes from showing up on screen
1340                     nextBrogueEvent(&theEvent, true, false, true);
1341                     if (rogue.gameHasEnded || theEvent.eventType == KEYSTROKE && theEvent.param1 == ESCAPE_KEY) {
1342                         return false;
1343                     }
1344                 }
1345                 rogue.playbackFastForward = true;
1346                 previousRecordingLocation = recordingLocation;
1347             }
1348         }
1349     }
1350 
1351     if (!rogue.gameHasEnded && !rogue.playbackOOS) {
1352         switchToPlaying();
1353         recordChar(SAVED_GAME_LOADED);
1354     }
1355     return true;
1356 }
1357 
1358 // the following functions are used to create human-readable descriptions of playback files for debugging purposes
1359 
describeKeystroke(unsigned char key,char * description)1360 void describeKeystroke(unsigned char key, char *description) {
1361     short i;
1362     long c;
1363     const long keyList[50] = {UP_KEY, DOWN_KEY, LEFT_KEY, RIGHT_KEY, UP_ARROW, LEFT_ARROW,
1364         DOWN_ARROW, RIGHT_ARROW, UPLEFT_KEY, UPRIGHT_KEY, DOWNLEFT_KEY, DOWNRIGHT_KEY,
1365         DESCEND_KEY, ASCEND_KEY, REST_KEY, AUTO_REST_KEY, SEARCH_KEY, INVENTORY_KEY,
1366         ACKNOWLEDGE_KEY, EQUIP_KEY, UNEQUIP_KEY, APPLY_KEY, THROW_KEY, RELABEL_KEY, DROP_KEY, CALL_KEY,
1367         //FIGHT_KEY, FIGHT_TO_DEATH_KEY,
1368         BROGUE_HELP_KEY, DISCOVERIES_KEY, RETURN_KEY,
1369         EXPLORE_KEY, AUTOPLAY_KEY, SEED_KEY, EASY_MODE_KEY, ESCAPE_KEY,
1370         RETURN_KEY, DELETE_KEY, TAB_KEY, PERIOD_KEY, VIEW_RECORDING_KEY, NUMPAD_0,
1371         NUMPAD_1, NUMPAD_2, NUMPAD_3, NUMPAD_4, NUMPAD_5, NUMPAD_6, NUMPAD_7, NUMPAD_8,
1372         NUMPAD_9, UNKNOWN_KEY};
1373     const char descList[51][30] = {"up", "down", "left", "right", "up arrow", "left arrow",
1374         "down arrow", "right arrow", "upleft", "upright", "downleft", "downright",
1375         "descend", "ascend", "rest", "auto rest", "search", "inventory", "acknowledge",
1376         "equip", "unequip", "apply", "throw", "relabel", "drop", "call",
1377         //"fight", "fight to death",
1378         "help", "discoveries", "repeat travel", "explore", "autoplay", "seed",
1379         "easy mode", "escape", "return", "delete", "tab", "period", "open file",
1380         "numpad 0", "numpad 1", "numpad 2", "numpad 3", "numpad 4", "numpad 5", "numpad 6",
1381         "numpad 7", "numpad 8", "numpad 9", "unknown", "ERROR"};
1382 
1383     c = uncompressKeystroke(key);
1384     for (i=0; i < 50 && keyList[i] != c; i++);
1385     if (key >= 32 && key <= 126) {
1386         sprintf(description, "Key: %c\t(%s)", key, descList[i]);
1387     } else {
1388         sprintf(description, "Key: %i\t(%s)", key, descList[i]);
1389     }
1390 }
1391 
appendModifierKeyDescription(char * description)1392 void appendModifierKeyDescription(char *description) {
1393     unsigned char c = recallChar();
1394 
1395     if (c & Fl(1)) {
1396         strcat(description, " + CRTL");
1397     }
1398     if (c & Fl(2)) {
1399         strcat(description, " + SHIFT");
1400     }
1401 }
1402 
1403 // Deprecated! Only used to parse recordings, a debugging feature.
selectFile(char * prompt,char * defaultName,char * suffix)1404 boolean selectFile(char *prompt, char *defaultName, char *suffix) {
1405     boolean retval;
1406     char newFilePath[BROGUE_FILENAME_MAX];
1407 
1408     retval = false;
1409 
1410     if (chooseFile(newFilePath, prompt, defaultName, suffix)) {
1411         if (openFile(newFilePath)) {
1412             retval = true;
1413         } else {
1414             confirmMessages();
1415             message("File not found.", 0);
1416             retval = false;
1417         }
1418     }
1419     return retval;
1420 }
1421 
parseFile()1422 void parseFile() {
1423     FILE *descriptionFile;
1424     unsigned long oldFileLoc, oldRecLoc, oldLength, oldBufLoc, i, numTurns, numDepths, fileLength, startLoc;
1425     uint64_t seed;
1426     unsigned char c;
1427     char description[1000], versionString[500];
1428     short x, y;
1429 
1430     if (selectFile("Parse recording: ", "Recording.broguerec", "")) {
1431 
1432         oldFileLoc = positionInPlaybackFile;
1433         oldRecLoc = recordingLocation;
1434         oldBufLoc = locationInRecordingBuffer;
1435         oldLength = lengthOfPlaybackFile;
1436 
1437         positionInPlaybackFile = 0;
1438         locationInRecordingBuffer = 0;
1439         recordingLocation = 0;
1440         lengthOfPlaybackFile = 10000000; // hack so that the recalls don't freak out
1441         fillBufferFromFile();
1442 
1443         descriptionFile = fopen("Recording Description.txt", "w");
1444 
1445         for (i=0; i<16; i++) {
1446             versionString[i] = recallChar();
1447         }
1448 
1449         seed        = recallNumber(8);
1450         numTurns    = recallNumber(4);
1451         numDepths   = recallNumber(4);
1452         fileLength  = recallNumber(4);
1453 
1454         fprintf(descriptionFile, "Parsed file \"%s\":\n\tVersion: %s\n\tSeed: %li\n\tNumber of turns: %li\n\tNumber of depth changes: %li\n\tFile length: %li\n",
1455                 currentFilePath,
1456                 versionString,
1457                 seed,
1458                 numTurns,
1459                 numDepths,
1460                 fileLength);
1461         for (i=0; recordingLocation < fileLength; i++) {
1462             startLoc = recordingLocation;
1463             c = recallChar();
1464             switch (c) {
1465                 case KEYSTROKE:
1466                     describeKeystroke(recallChar(), description);
1467                     appendModifierKeyDescription(description);
1468                     break;
1469                 case MOUSE_UP:
1470                 case MOUSE_DOWN:
1471                 case MOUSE_ENTERED_CELL:
1472                     x = (short) recallChar();
1473                     y = (short) recallChar();
1474                     sprintf(description, "Mouse click: (%i, %i)", x, y);
1475                     appendModifierKeyDescription(description);
1476                     break;
1477                 case RNG_CHECK:
1478                     sprintf(description, "\tRNG check: %i", (short) recallChar());
1479                     break;
1480                 case SAVED_GAME_LOADED:
1481                     strcpy(description, "Saved game loaded");
1482                     break;
1483                 default:
1484                     sprintf(description, "UNKNOWN EVENT TYPE: %i", (short) c);
1485                     break;
1486             }
1487             fprintf(descriptionFile, "\nEvent %li, loc %li, length %li:%s\t%s", i, startLoc, recordingLocation - startLoc, (i < 10 ? " " : ""), description);
1488         }
1489 
1490         fclose(descriptionFile);
1491 
1492         positionInPlaybackFile = oldFileLoc;
1493         recordingLocation = oldRecLoc;
1494         lengthOfPlaybackFile = oldLength;
1495         locationInRecordingBuffer = oldBufLoc;
1496         message("File parsed.", 0);
1497     } else {
1498         confirmMessages();
1499     }
1500 }
1501 
RNGLog(char * message)1502 void RNGLog(char *message) {
1503 #ifdef AUDIT_RNG
1504     fputs(message, RNGLogFile);
1505 #endif
1506 }
1507