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