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