1 /*
2  *  Buttons.c
3  *  Brogue
4  *
5  *  Created by Brian Walker on 1/14/12.
6  *  Copyright 2012. All rights reserved.
7  *
8  *  This file is part of Brogue.
9  *
10  *  This program is free software: you can redistribute it and/or modify
11  *  it under the terms of the GNU Affero General Public License as
12  *  published by the Free Software Foundation, either version 3 of the
13  *  License, or (at your option) any later version.
14  *
15  *  This program is distributed in the hope that it will be useful,
16  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
17  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18  *  GNU Affero General Public License for more details.
19  *
20  *  You should have received a copy of the GNU Affero General Public License
21  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
22  */
23 
24 #include "Rogue.h"
25 #include "IncludeGlobals.h"
26 #include <time.h>
27 #include <limits.h>
28 
29 #define MENU_FLAME_PRECISION_FACTOR     10
30 #define MENU_FLAME_RISE_SPEED           50
31 #define MENU_FLAME_SPREAD_SPEED         20
32 #define MENU_FLAME_COLOR_DRIFT_SPEED    500
33 #define MENU_FLAME_FADE_SPEED           20
34 #define MENU_FLAME_UPDATE_DELAY         50
35 #define MENU_FLAME_ROW_PADDING          2
36 #define MENU_TITLE_OFFSET_X             (-4)
37 #define MENU_TITLE_OFFSET_Y             (-1)
38 
39 #define MENU_FLAME_COLOR_SOURCE_COUNT   1136
40 
41 #define MENU_FLAME_DENOMINATOR          (100 + MENU_FLAME_RISE_SPEED + MENU_FLAME_SPREAD_SPEED)
42 
43 
drawMenuFlames(signed short flames[COLS][(ROWS+MENU_FLAME_ROW_PADDING)][3],unsigned char mask[COLS][ROWS])44 void drawMenuFlames(signed short flames[COLS][(ROWS + MENU_FLAME_ROW_PADDING)][3], unsigned char mask[COLS][ROWS]) {
45     short i, j, versionStringLength;
46     color tempColor = {0};
47     const color *maskColor = &black;
48     char dchar;
49 
50     versionStringLength = strLenWithoutEscapes(BROGUE_VERSION_STRING);
51 
52     for (j=0; j<ROWS; j++) {
53         for (i=0; i<COLS; i++) {
54             if (j == ROWS - 1 && i >= COLS - versionStringLength) {
55                 dchar = BROGUE_VERSION_STRING[i - (COLS - versionStringLength)];
56             } else {
57                 dchar = ' ';
58             }
59 
60             if (mask[i][j] == 100) {
61                 plotCharWithColor(dchar, i, j, &veryDarkGray, maskColor);
62             } else {
63                 tempColor = black;
64                 tempColor.red   = flames[i][j][0] / MENU_FLAME_PRECISION_FACTOR;
65                 tempColor.green = flames[i][j][1] / MENU_FLAME_PRECISION_FACTOR;
66                 tempColor.blue  = flames[i][j][2] / MENU_FLAME_PRECISION_FACTOR;
67                 if (mask[i][j] > 0) {
68                     applyColorAverage(&tempColor, maskColor, mask[i][j]);
69                 }
70                 plotCharWithColor(dchar, i, j, &veryDarkGray, &tempColor);
71             }
72         }
73     }
74 }
75 
updateMenuFlames(const color * colors[COLS][(ROWS+MENU_FLAME_ROW_PADDING)],signed short colorSources[MENU_FLAME_COLOR_SOURCE_COUNT][4],signed short flames[COLS][(ROWS+MENU_FLAME_ROW_PADDING)][3])76 void updateMenuFlames(const color *colors[COLS][(ROWS + MENU_FLAME_ROW_PADDING)],
77                       signed short colorSources[MENU_FLAME_COLOR_SOURCE_COUNT][4],
78                       signed short flames[COLS][(ROWS + MENU_FLAME_ROW_PADDING)][3]) {
79 
80     short i, j, k, l, x, y;
81     signed short tempFlames[COLS][3];
82     short colorSourceNumber, rand;
83 
84     colorSourceNumber = 0;
85     for (j=0; j<(ROWS + MENU_FLAME_ROW_PADDING); j++) {
86         // Make a temp copy of the current row.
87         for (i=0; i<COLS; i++) {
88             for (k=0; k<3; k++) {
89                 tempFlames[i][k] = flames[i][j][k];
90             }
91         }
92 
93         for (i=0; i<COLS; i++) {
94             // Each cell is the weighted average of the three color values below and itself.
95             // Weight of itself: 100
96             // Weight of left and right neighbors: MENU_FLAME_SPREAD_SPEED / 2 each
97             // Weight of below cell: MENU_FLAME_RISE_SPEED
98             // Divisor: 100 + MENU_FLAME_SPREAD_SPEED + MENU_FLAME_RISE_SPEED
99 
100             // Itself:
101             for (k=0; k<3; k++) {
102                 flames[i][j][k] = 100 * flames[i][j][k] / MENU_FLAME_DENOMINATOR;
103             }
104 
105             // Left and right neighbors:
106             for (l = -1; l <= 1; l += 2) {
107                 x = i + l;
108                 if (x == -1) {
109                     x = COLS - 1;
110                 } else if (x == COLS) {
111                     x = 0;
112                 }
113                 for (k=0; k<3; k++) {
114                     flames[i][j][k] += MENU_FLAME_SPREAD_SPEED * tempFlames[x][k] / 2 / MENU_FLAME_DENOMINATOR;
115                 }
116             }
117 
118             // Below:
119             y = j + 1;
120             if (y < (ROWS + MENU_FLAME_ROW_PADDING)) {
121                 for (k=0; k<3; k++) {
122                     flames[i][j][k] += MENU_FLAME_RISE_SPEED * flames[i][y][k] / MENU_FLAME_DENOMINATOR;
123                 }
124             }
125 
126             // Fade a little:
127             for (k=0; k<3; k++) {
128                 flames[i][j][k] = (1000 - MENU_FLAME_FADE_SPEED) * flames[i][j][k] / 1000;
129             }
130 
131             if (colors[i][j]) {
132                 // If it's a color source tile:
133 
134                 // First, cause the color to drift a little.
135                 for (k=0; k<4; k++) {
136                     colorSources[colorSourceNumber][k] += rand_range(-MENU_FLAME_COLOR_DRIFT_SPEED, MENU_FLAME_COLOR_DRIFT_SPEED);
137                     colorSources[colorSourceNumber][k] = clamp(colorSources[colorSourceNumber][k], 0, 1000);
138                 }
139 
140                 // Then, add the color to this tile's flames.
141                 rand = colors[i][j]->rand * colorSources[colorSourceNumber][0] / 1000;
142                 flames[i][j][0] += (colors[i][j]->red   + (colors[i][j]->redRand    * colorSources[colorSourceNumber][1] / 1000) + rand) * MENU_FLAME_PRECISION_FACTOR;
143                 flames[i][j][1] += (colors[i][j]->green + (colors[i][j]->greenRand  * colorSources[colorSourceNumber][2] / 1000) + rand) * MENU_FLAME_PRECISION_FACTOR;
144                 flames[i][j][2] += (colors[i][j]->blue  + (colors[i][j]->blueRand   * colorSources[colorSourceNumber][3] / 1000) + rand) * MENU_FLAME_PRECISION_FACTOR;
145 
146                 colorSourceNumber++;
147             }
148         }
149     }
150 }
151 
152 // Takes a grid of values, each of which is 0 or 100, and fills in some middle values in the interstices.
antiAlias(unsigned char mask[COLS][ROWS])153 void antiAlias(unsigned char mask[COLS][ROWS]) {
154     short i, j, x, y, dir, nbCount;
155     const short intensity[5] = {0, 0, 35, 50, 60};
156 
157     for (i=0; i<COLS; i++) {
158         for (j=0; j<ROWS; j++) {
159             if (mask[i][j] < 100) {
160                 nbCount = 0;
161                 for (dir=0; dir<4; dir++) {
162                     x = i + nbDirs[dir][0];
163                     y = j + nbDirs[dir][1];
164                     if (coordinatesAreInWindow(x, y) && mask[x][y] == 100) {
165                         nbCount++;
166                     }
167                 }
168                 mask[i][j] = intensity[nbCount];
169             }
170         }
171     }
172 }
173 
174 #define MENU_TITLE_WIDTH    74
175 #define MENU_TITLE_HEIGHT   19
176 
initializeMenuFlames(boolean includeTitle,const color * colors[COLS][(ROWS+MENU_FLAME_ROW_PADDING)],color colorStorage[COLS],signed short colorSources[MENU_FLAME_COLOR_SOURCE_COUNT][4],signed short flames[COLS][(ROWS+MENU_FLAME_ROW_PADDING)][3],unsigned char mask[COLS][ROWS])177 void initializeMenuFlames(boolean includeTitle,
178                           const color *colors[COLS][(ROWS + MENU_FLAME_ROW_PADDING)],
179                           color colorStorage[COLS],
180                           signed short colorSources[MENU_FLAME_COLOR_SOURCE_COUNT][4],
181                           signed short flames[COLS][(ROWS + MENU_FLAME_ROW_PADDING)][3],
182                           unsigned char mask[COLS][ROWS]) {
183     short i, j, k, colorSourceCount;
184     const char title[MENU_TITLE_HEIGHT][MENU_TITLE_WIDTH+1] = {
185         "########   ########       ######         #######   ####     ###  #########",
186         " ##   ###   ##   ###    ##     ###     ##      ##   ##       #    ##     #",
187         " ##    ##   ##    ##   ##       ###   ##        #   ##       #    ##     #",
188         " ##    ##   ##    ##   #    #    ##   #         #   ##       #    ##      ",
189         " ##    ##   ##    ##  ##   ##     ## ##             ##       #    ##    # ",
190         " ##   ##    ##   ##   ##   ###    ## ##             ##       #    ##    # ",
191         " ######     ## ###    ##   ####   ## ##             ##       #    ####### ",
192         " ##    ##   ##  ##    ##   ####   ## ##             ##       #    ##    # ",
193         " ##     ##  ##   ##   ##    ###   ## ##      #####  ##       #    ##    # ",
194         " ##     ##  ##   ##   ###    ##   ## ###       ##   ##       #    ##      ",
195         " ##     ##  ##    ##   ##    #    #   ##       ##   ##       #    ##      ",
196         " ##     ##  ##    ##   ###       ##   ###      ##   ###      #    ##     #",
197         " ##    ##   ##     ##   ###     ##     ###    ###    ###    #     ##     #",
198         "########   ####    ###    ######         #####        ######     #########",
199         "                            ##                                            ",
200         "                        ##########                                        ",
201         "                            ##                                            ",
202         "                            ##                                            ",
203         "                           ####                                           ",
204     };
205 
206     for (i=0; i<COLS; i++) {
207         for (j=0; j<ROWS; j++) {
208             mask[i][j] = 0;
209         }
210     }
211 
212     for (i=0; i<COLS; i++) {
213         for (j=0; j<(ROWS + MENU_FLAME_ROW_PADDING); j++) {
214             colors[i][j] = NULL;
215             for (k=0; k<3; k++) {
216                 flames[i][j][k] = 0;
217             }
218         }
219     }
220 
221     // Seed source color random components.
222     for (i=0; i<MENU_FLAME_COLOR_SOURCE_COUNT; i++) {
223         for (k=0; k<4; k++) {
224             colorSources[i][k] = rand_range(0, 1000);
225         }
226     }
227 
228     // Put some flame source along the bottom row.
229     colorSourceCount = 0;
230     for (i=0; i<COLS; i++) {
231         colorStorage[colorSourceCount] = flameSourceColor;
232         applyColorAverage(&(colorStorage[colorSourceCount]), &flameSourceColorSecondary, 100 - (smoothHiliteGradient(i, COLS - 1) + 25));
233 
234         colors[i][(ROWS + MENU_FLAME_ROW_PADDING)-1] = &(colorStorage[colorSourceCount]);
235         colorSourceCount++;
236     }
237 
238     if (includeTitle) {
239         // Wreathe the title in flames, and mask it in black.
240         for (i=0; i<MENU_TITLE_WIDTH; i++) {
241             for (j=0; j<MENU_TITLE_HEIGHT; j++) {
242                 if (title[j][i] != ' ') {
243                     colors[(COLS - MENU_TITLE_WIDTH)/2 + i + MENU_TITLE_OFFSET_X][(ROWS - MENU_TITLE_HEIGHT)/2 + j + MENU_TITLE_OFFSET_Y] = &flameTitleColor;
244                     colorSourceCount++;
245                     mask[(COLS - MENU_TITLE_WIDTH)/2 + i + MENU_TITLE_OFFSET_X][(ROWS - MENU_TITLE_HEIGHT)/2 + j + MENU_TITLE_OFFSET_Y] = 100;
246                 }
247             }
248         }
249 
250         // Anti-alias the mask.
251         antiAlias(mask);
252     }
253 
254     brogueAssert(colorSourceCount <= MENU_FLAME_COLOR_SOURCE_COUNT);
255 
256     // Simulate the background flames for a while
257     for (i=0; i<100; i++) {
258         updateMenuFlames(colors, colorSources, flames);
259     }
260 
261 }
262 
titleMenu()263 void titleMenu() {
264     signed short flames[COLS][(ROWS + MENU_FLAME_ROW_PADDING)][3]; // red, green and blue
265     signed short colorSources[MENU_FLAME_COLOR_SOURCE_COUNT][4]; // red, green, blue, and rand, one for each color source (no more than MENU_FLAME_COLOR_SOURCE_COUNT).
266     const color *colors[COLS][(ROWS + MENU_FLAME_ROW_PADDING)];
267     color colorStorage[COLS];
268     unsigned char mask[COLS][ROWS];
269     boolean controlKeyWasDown = false;
270 
271     short i, b, x, y, button;
272     buttonState state;
273     brogueButton buttons[6];
274     char whiteColorEscape[10] = "";
275     char goldColorEscape[10] = "";
276     char newGameText[100] = "", customNewGameText[100] = "";
277     rogueEvent theEvent;
278     enum NGCommands buttonCommands[6] = {NG_NEW_GAME, NG_OPEN_GAME, NG_VIEW_RECORDING, NG_HIGH_SCORES, NG_QUIT};
279 
280     cellDisplayBuffer shadowBuf[COLS][ROWS];
281 
282     // Initialize the RNG so the flames aren't always the same.
283 
284     seedRandomGenerator(0);
285 
286     // Empty nextGamePath and nextGameSeed so that the buttons don't try to load an old game path or seed.
287     rogue.nextGamePath[0] = '\0';
288     rogue.nextGameSeed = 0;
289 
290     // Initialize the title menu buttons.
291     encodeMessageColor(whiteColorEscape, 0, &white);
292     encodeMessageColor(goldColorEscape, 0, KEYBOARD_LABELS ? &itemMessageColor : &white);
293     sprintf(newGameText, "      %sN%sew Game      ", goldColorEscape, whiteColorEscape);
294     sprintf(customNewGameText, " %sN%sew Game (custom) ", goldColorEscape, whiteColorEscape);
295     b = 0;
296     button = -1;
297 
298     initializeButton(&(buttons[b]));
299     strcpy(buttons[b].text, newGameText);
300     buttons[b].hotkey[0] = 'n';
301     buttons[b].hotkey[1] = 'N';
302     b++;
303 
304     initializeButton(&(buttons[b]));
305     sprintf(buttons[b].text, "     %sO%spen Game      ", goldColorEscape, whiteColorEscape);
306     buttons[b].hotkey[0] = 'o';
307     buttons[b].hotkey[1] = 'O';
308     b++;
309 
310     initializeButton(&(buttons[b]));
311     sprintf(buttons[b].text, "   %sV%siew Recording   ", goldColorEscape, whiteColorEscape);
312     buttons[b].hotkey[0] = 'v';
313     buttons[b].hotkey[1] = 'V';
314     b++;
315 
316     initializeButton(&(buttons[b]));
317     sprintf(buttons[b].text, "    %sH%sigh Scores     ", goldColorEscape, whiteColorEscape);
318     buttons[b].hotkey[0] = 'h';
319     buttons[b].hotkey[1] = 'H';
320     b++;
321 
322     initializeButton(&(buttons[b]));
323     sprintf(buttons[b].text, "        %sQ%suit        ", goldColorEscape, whiteColorEscape);
324     buttons[b].hotkey[0] = 'q';
325     buttons[b].hotkey[1] = 'Q';
326     b++;
327 
328     x = COLS - 1 - 20 - 2;
329     y = ROWS - 1;
330     for (i = b-1; i >= 0; i--) {
331         y -= 2;
332         buttons[i].x = x;
333         buttons[i].y = y;
334         buttons[i].buttonColor = titleButtonColor;
335         buttons[i].flags |= B_WIDE_CLICK_AREA;
336     }
337 
338     blackOutScreen();
339     clearDisplayBuffer(shadowBuf);
340     initializeButtonState(&state, buttons, b, x, y, 20, b*2-1);
341     rectangularShading(x, y, 20, b*2-1, &black, INTERFACE_OPACITY, shadowBuf);
342     drawButtonsInState(&state);
343 
344     initializeMenuFlames(true, colors, colorStorage, colorSources, flames, mask);
345     rogue.creaturesWillFlashThisTurn = false; // total unconscionable hack
346 
347     do {
348         if (isApplicationActive()) {
349             // Revert the display.
350             overlayDisplayBuffer(state.rbuf, NULL);
351 
352             if (!controlKeyWasDown && controlKeyIsDown()) {
353                 strcpy(state.buttons[0].text, customNewGameText);
354                 drawButtonsInState(&state);
355                 buttonCommands[0] = NG_NEW_GAME_WITH_SEED;
356                 controlKeyWasDown = true;
357             } else if (controlKeyWasDown && !controlKeyIsDown()) {
358                 strcpy(state.buttons[0].text, newGameText);
359                 drawButtonsInState(&state);
360                 buttonCommands[0] = NG_NEW_GAME;
361                 controlKeyWasDown = false;
362             }
363 
364             // Update the display.
365             updateMenuFlames(colors, colorSources, flames);
366             drawMenuFlames(flames, mask);
367             overlayDisplayBuffer(shadowBuf, NULL);
368             overlayDisplayBuffer(state.dbuf, NULL);
369 
370             // Pause briefly.
371             if (pauseBrogue(MENU_FLAME_UPDATE_DELAY)) {
372                 // There was input during the pause! Get the input.
373                 nextBrogueEvent(&theEvent, true, false, true);
374 
375                 // Process the input.
376                 button = processButtonInput(&state, NULL, &theEvent);
377             }
378 
379         } else {
380             pauseBrogue(64);
381         }
382     } while (button == -1 && rogue.nextGame == NG_NOTHING);
383     drawMenuFlames(flames, mask);
384     if (button != -1) {
385         if (button == 0 && controlKeyIsDown()) {
386             // Should fix an issue with Linux/Windows ports that require moving the mouse after
387             // pressing control to get the button to change.
388             rogue.nextGame = NG_NEW_GAME_WITH_SEED;
389         } else {
390             rogue.nextGame = buttonCommands[button];
391         }
392     }
393 }
394 
395 // Closes Brogue without any further prompts, animations, or user interaction.
quitImmediately()396 void quitImmediately() {
397     // If we are recording a game, save it.
398     if (rogue.recording) {
399         flushBufferToFile();
400         if (rogue.gameInProgress && !rogue.quit && !rogue.gameHasEnded) {
401             // Game isn't over yet, create a savegame.
402             saveGameNoPrompt();
403         } else {
404             // Save it as a recording.
405             char path[BROGUE_FILENAME_MAX];
406             saveRecordingNoPrompt(path);
407         }
408     }
409     exit(0);
410 }
411 
dialogAlert(char * message)412 void dialogAlert(char *message) {
413     cellDisplayBuffer rbuf[COLS][ROWS];
414 
415     brogueButton OKButton;
416     initializeButton(&OKButton);
417     strcpy(OKButton.text, "     OK     ");
418     OKButton.hotkey[0] = RETURN_KEY;
419     OKButton.hotkey[1] = ACKNOWLEDGE_KEY;
420     printTextBox(message, COLS/3, ROWS/3, COLS/3, &white, &interfaceBoxColor, rbuf, &OKButton, 1);
421     overlayDisplayBuffer(rbuf, NULL);
422 }
423 
stringsExactlyMatch(const char * string1,const char * string2)424 boolean stringsExactlyMatch(const char *string1, const char *string2) {
425     short i;
426     for (i=0; string1[i] && string2[i]; i++) {
427         if (string1[i] != string2[i]) {
428             return false;
429         }
430     }
431     return string1[i] == string2[i];
432 }
433 
434 // Used to compare the dates of two fileEntry variables
435 // Returns (int):
436 //      < 0 if 'b' date is lesser than 'a' date
437 //      = 0 if 'b' date is equal to 'a' date,
438 //      > 0 if 'b' date is greater than 'a' date
fileEntryCompareDates(const void * a,const void * b)439 int fileEntryCompareDates(const void *a, const void *b) {
440     fileEntry *f1 = (fileEntry *)a;
441     fileEntry *f2 = (fileEntry *)b;
442     time_t t1, t2;
443     double diff;
444 
445     t1 = mktime(&f1->date);
446     t2 = mktime(&f2->date);
447     diff = difftime(t2, t1);
448 
449     //char date_f1[11];
450     //char date_f2[11];
451     //strftime(date_f1, sizeof(date_f1), DATE_FORMAT, &f1->date);
452     //strftime(date_f2, sizeof(date_f2), DATE_FORMAT, &f2->date);
453     //printf("\nf1: %s\t%s",date_f1,f1->path);
454     //printf("\nf2: %s\t%s",date_f2,f2->path);
455     //printf("\ndiff: %f\n", diff);
456 
457     return (int)diff;
458 }
459 
460 #define FILES_ON_PAGE_MAX               (min(26, ROWS - 7)) // Two rows (top and bottom) for flames, two rows for border, one for prompt, one for heading.
461 #define MAX_FILENAME_DISPLAY_LENGTH     53
dialogChooseFile(char * path,const char * suffix,const char * prompt)462 boolean dialogChooseFile(char *path, const char *suffix, const char *prompt) {
463     short i, j, count, x, y, width, height, suffixLength, pathLength, maxPathLength, currentPageStart;
464     brogueButton buttons[FILES_ON_PAGE_MAX + 2];
465     fileEntry *files;
466     boolean retval = false, again;
467     cellDisplayBuffer dbuf[COLS][ROWS], rbuf[COLS][ROWS];
468     color *dialogColor = &interfaceBoxColor;
469     char *membuf;
470     char fileDate [11];
471 
472     suffixLength = strlen(suffix);
473     files = listFiles(&count, &membuf);
474     copyDisplayBuffer(rbuf, displayBuffer);
475     maxPathLength = strLenWithoutEscapes(prompt);
476 
477     // First, we want to filter the list by stripping out any filenames that do not end with suffix.
478     // i is the entry we're testing, and j is the entry that we move it to if it qualifies.
479     for (i=0, j=0; i<count; i++) {
480         pathLength = strlen(files[i].path);
481         //printf("\nString 1: %s", &(files[i].path[(max(0, pathLength - suffixLength))]));
482         if (stringsExactlyMatch(&(files[i].path[(max(0, pathLength - suffixLength))]), suffix)) {
483 
484             // This file counts!
485             if (i > j) {
486                 files[j] = files[i];
487                 //strftime(fileDate, sizeof(fileDate), DATE_FORMAT, &files[j].date);
488                 //printf("\nMatching file: %s\twith date: %s", files[j].path, fileDate);
489             }
490             j++;
491 
492             // Keep track of the longest length.
493             if (min(pathLength, MAX_FILENAME_DISPLAY_LENGTH) + 10 > maxPathLength) {
494                 maxPathLength = min(pathLength, MAX_FILENAME_DISPLAY_LENGTH) + 10;
495             }
496         }
497     }
498     count = j;
499 
500     // Once we have all relevant files, we sort them by date descending
501     qsort(files, count, sizeof(fileEntry), &fileEntryCompareDates);
502 
503     currentPageStart = 0;
504 
505     do { // Repeat to permit scrolling.
506         again = false;
507 
508         for (i=0; i<min(count - currentPageStart, FILES_ON_PAGE_MAX); i++) {
509             initializeButton(&(buttons[i]));
510             buttons[i].flags &= ~(B_WIDE_CLICK_AREA | B_GRADIENT);
511             buttons[i].buttonColor = *dialogColor;
512             if (KEYBOARD_LABELS) {
513                 sprintf(buttons[i].text, "%c) ", 'a' + i);
514             } else {
515                 buttons[i].text[0] = '\0';
516             }
517             strncat(buttons[i].text, files[currentPageStart+i].path, MAX_FILENAME_DISPLAY_LENGTH);
518 
519             // Clip off the file suffix from the button text.
520             buttons[i].text[strlen(buttons[i].text) - suffixLength] = '\0'; // Snip!
521             buttons[i].hotkey[0] = 'a' + i;
522             buttons[i].hotkey[1] = 'A' + i;
523 
524             // Clip the filename length if necessary.
525             if (strlen(buttons[i].text) > MAX_FILENAME_DISPLAY_LENGTH) {
526                 strcpy(&(buttons[i].text[MAX_FILENAME_DISPLAY_LENGTH - 3]), "...");
527             }
528 
529             //strftime(fileDate, sizeof(fileDate), DATE_FORMAT, &files[currentPageStart+i].date);
530             //printf("\nFound file: %s, with date: %s", files[currentPageStart+i].path, fileDate);
531         }
532 
533         x = (COLS - maxPathLength) / 2;
534         width = maxPathLength;
535         height = min(count - currentPageStart, FILES_ON_PAGE_MAX) + 2;
536         y = max(4, (ROWS - height) / 2);
537 
538         for (i=0; i<min(count - currentPageStart, FILES_ON_PAGE_MAX); i++) {
539             pathLength = strlen(buttons[i].text);
540             for (j=pathLength; j<(width - 10); j++) {
541                 buttons[i].text[j] = ' ';
542             }
543             buttons[i].text[j] = '\0';
544             strftime(fileDate, sizeof(fileDate), DATE_FORMAT, &files[currentPageStart+i].date);
545             strcpy(&(buttons[i].text[j]), fileDate);
546             buttons[i].x = x;
547             buttons[i].y = y + 1 + i;
548         }
549 
550         if (count > FILES_ON_PAGE_MAX) {
551             // Create up and down arrows.
552             initializeButton(&(buttons[i]));
553             strcpy(buttons[i].text, "     *     ");
554             buttons[i].symbol[0] = G_UP_ARROW;
555             if (currentPageStart <= 0) {
556                 buttons[i].flags &= ~(B_ENABLED | B_DRAW);
557             } else {
558                 buttons[i].hotkey[0] = UP_ARROW;
559                 buttons[i].hotkey[1] = NUMPAD_8;
560                 buttons[i].hotkey[2] = PAGE_UP_KEY;
561             }
562             buttons[i].x = x + (width - 11)/2;
563             buttons[i].y = y;
564 
565             i++;
566             initializeButton(&(buttons[i]));
567             strcpy(buttons[i].text, "     *     ");
568             buttons[i].symbol[0] = G_DOWN_ARROW;
569             if (currentPageStart + FILES_ON_PAGE_MAX >= count) {
570                 buttons[i].flags &= ~(B_ENABLED | B_DRAW);
571             } else {
572                 buttons[i].hotkey[0] = DOWN_ARROW;
573                 buttons[i].hotkey[1] = NUMPAD_2;
574                 buttons[i].hotkey[2] = PAGE_DOWN_KEY;
575             }
576             buttons[i].x = x + (width - 11)/2;
577             buttons[i].y = y + i;
578         }
579 
580         if (count) {
581             clearDisplayBuffer(dbuf);
582             printString(prompt, x, y - 1, &itemMessageColor, dialogColor, dbuf);
583             rectangularShading(x - 1, y - 1, width + 1, height + 1, dialogColor, INTERFACE_OPACITY, dbuf);
584             overlayDisplayBuffer(dbuf, NULL);
585 
586 //          for (j=0; j<min(count - currentPageStart, FILES_ON_PAGE_MAX); j++) {
587 //              strftime(fileDate, sizeof(fileDate), DATE_FORMAT, &files[currentPageStart+j].date);
588 //              printf("\nSanity check BEFORE: %s, with date: %s", files[currentPageStart+j].path, fileDate);
589 //              printf("\n   (button name)Sanity check BEFORE: %s", buttons[j].text);
590 //          }
591 
592             i = buttonInputLoop(buttons,
593                                 min(count - currentPageStart, FILES_ON_PAGE_MAX) + (count > FILES_ON_PAGE_MAX ? 2 : 0),
594                                 x,
595                                 y,
596                                 width,
597                                 height,
598                                 NULL);
599 
600 //          for (j=0; j<min(count - currentPageStart, FILES_ON_PAGE_MAX); j++) {
601 //              strftime(fileDate, sizeof(fileDate), DATE_FORMAT, &files[currentPageStart+j].date);
602 //              printf("\nSanity check AFTER: %s, with date: %s", files[currentPageStart+j].path, fileDate);
603 //              printf("\n   (button name)Sanity check AFTER: %s", buttons[j].text);
604 //          }
605 
606             overlayDisplayBuffer(rbuf, NULL);
607 
608             if (i < min(count - currentPageStart, FILES_ON_PAGE_MAX)) {
609                 if (i >= 0) {
610                     retval = true;
611                     strcpy(path, files[currentPageStart+i].path);
612                 } else { // i is -1
613                     retval = false;
614                 }
615             } else if (i == min(count - currentPageStart, FILES_ON_PAGE_MAX)) { // Up arrow
616                 again = true;
617                 currentPageStart -= FILES_ON_PAGE_MAX;
618             } else if (i == min(count - currentPageStart, FILES_ON_PAGE_MAX) + 1) { // Down arrow
619                 again = true;
620                 currentPageStart += FILES_ON_PAGE_MAX;
621             }
622         }
623 
624     } while (again);
625 
626     free(files);
627     free(membuf);
628 
629     if (count == 0) {
630         dialogAlert("No applicable files found.");
631         return false;
632     } else {
633         return retval;
634     }
635 }
636 
637 // This is the basic program loop.
638 // When the program launches, or when a game ends, you end up here.
639 // If the player has already said what he wants to do next
640 // (by storing it in rogue.nextGame -- possibilities listed in enum NGCommands),
641 // we'll do it. The path (rogue.nextGamePath) is essentially a parameter for this command, and
642 // tells NG_VIEW_RECORDING and NG_OPEN_GAME which file to open. If there is a command but no
643 // accompanying path, and it's a command that should take a path, then pop up a dialog to have
644 // the player specify a path. If there is no command (i.e. if rogue.nextGame contains NG_NOTHING),
645 // then we'll display the title screen so the player can choose.
mainBrogueJunction()646 void mainBrogueJunction() {
647     rogueEvent theEvent;
648     char path[BROGUE_FILENAME_MAX], buf[100], seedDefault[100];
649     short i, j, k;
650 
651     // clear screen and display buffer
652     for (i=0; i<COLS; i++) {
653         for (j=0; j<ROWS; j++) {
654             displayBuffer[i][j].character = 0;
655             displayBuffer[i][j].opacity = 100;
656             for (k=0; k<3; k++) {
657                 displayBuffer[i][j].foreColorComponents[k] = 0;
658                 displayBuffer[i][j].backColorComponents[k] = 0;
659             }
660             plotCharWithColor(' ', i, j, &black, &black);
661         }
662     }
663 
664     initializeLaunchArguments(&rogue.nextGame, rogue.nextGamePath, &rogue.nextGameSeed);
665 
666     do {
667         rogue.gameHasEnded = false;
668         rogue.playbackFastForward = false;
669         rogue.playbackMode = false;
670         switch (rogue.nextGame) {
671             case NG_NOTHING:
672                 // Run the main menu to get a decision out of the player.
673                 titleMenu();
674                 break;
675             case NG_NEW_GAME:
676             case NG_NEW_GAME_WITH_SEED:
677                 rogue.nextGamePath[0] = '\0';
678                 randomNumbersGenerated = 0;
679 
680                 rogue.playbackMode = false;
681                 rogue.playbackFastForward = false;
682                 rogue.playbackBetweenTurns = false;
683 
684                 getAvailableFilePath(path, LAST_GAME_NAME, GAME_SUFFIX);
685                 strcat(path, GAME_SUFFIX);
686                 strcpy(currentFilePath, path);
687 
688                 if (rogue.nextGame == NG_NEW_GAME_WITH_SEED) {
689                     if (rogue.nextGameSeed == 0) { // Prompt for seed; default is the previous game's seed.
690                         if (previousGameSeed == 0) {
691                             seedDefault[0] = '\0';
692                         } else {
693                             sprintf(seedDefault, "%llu", (unsigned long long)previousGameSeed);
694                         }
695                         if (getInputTextString(buf, "Generate dungeon with seed number:",
696                                                20, // length of "18446744073709551615" (2^64 - 1)
697                                                seedDefault,
698                                                "",
699                                                TEXT_INPUT_NUMBERS,
700                                                true)
701                             && buf[0] != '\0') {
702                             if (!tryParseUint64(buf, &rogue.nextGameSeed)) {
703                                 // seed is too large, default to the largest possible seed
704                                 rogue.nextGameSeed = 18446744073709551615ULL;
705                             }
706                         } else {
707                             rogue.nextGame = NG_NOTHING;
708                             break; // Don't start a new game after all.
709                         }
710                     }
711                 } else {
712                     rogue.nextGameSeed = 0; // Seed based on clock.
713                 }
714 
715                 rogue.nextGame = NG_NOTHING;
716                 initializeRogue(rogue.nextGameSeed);
717                 startLevel(rogue.depthLevel, 1); // descending into level 1
718 
719                 mainInputLoop();
720                 if(serverMode) {
721                     rogue.nextGame = NG_QUIT;
722                 }
723                 freeEverything();
724                 break;
725             case NG_OPEN_GAME:
726                 rogue.nextGame = NG_NOTHING;
727                 path[0] = '\0';
728                 if (rogue.nextGamePath[0]) {
729                     strcpy(path, rogue.nextGamePath);
730                     rogue.nextGamePath[0] = '\0';
731                 } else {
732                     dialogChooseFile(path, GAME_SUFFIX, "Open saved game:");
733                     //chooseFile(path, "Open saved game: ", "Saved game", GAME_SUFFIX);
734                 }
735 
736                 if (openFile(path)) {
737                     if (loadSavedGame()) {
738                         mainInputLoop();
739                     }
740                     freeEverything();
741                 } else {
742                     //dialogAlert("File not found.");
743                 }
744                 rogue.playbackMode = false;
745                 rogue.playbackOOS = false;
746 
747                 if(serverMode) {
748                     rogue.nextGame = NG_QUIT;
749                 }
750                 break;
751             case NG_VIEW_RECORDING:
752                 rogue.nextGame = NG_NOTHING;
753 
754                 path[0] = '\0';
755                 if (rogue.nextGamePath[0]) {
756                     strcpy(path, rogue.nextGamePath);
757                     rogue.nextGamePath[0] = '\0';
758                 } else {
759                     dialogChooseFile(path, RECORDING_SUFFIX, "View recording:");
760                     //chooseFile(path, "View recording: ", "Recording", RECORDING_SUFFIX);
761                 }
762 
763                 if (openFile(path)) {
764                     randomNumbersGenerated = 0;
765                     rogue.playbackMode = true;
766                     initializeRogue(0); // Seed argument is ignored because we're in playback.
767                     if (!rogue.gameHasEnded) {
768                         startLevel(rogue.depthLevel, 1);
769                         rogue.playbackPaused = true;
770                         displayAnnotation(); // in case there's an annotation for turn 0
771                     }
772 
773                     while(!rogue.gameHasEnded && rogue.playbackMode) {
774                         if (rogue.playbackPaused) {
775                             rogue.playbackPaused = false;
776                             pausePlayback();
777                         }
778 #ifdef ENABLE_PLAYBACK_SWITCH
779                         // We are coming from the end of a recording the user has taken over.
780                         // No more event checks, that has already been handled
781                         if (rogue.gameHasEnded) {
782                             break;
783                         }
784 #endif
785                         rogue.RNG = RNG_COSMETIC; // dancing terrain colors can't influence recordings
786                         rogue.playbackBetweenTurns = true;
787                         nextBrogueEvent(&theEvent, false, true, false);
788                         rogue.RNG = RNG_SUBSTANTIVE;
789 
790                         executeEvent(&theEvent);
791                     }
792 
793                     freeEverything();
794                 } else {
795                     // announce file not found
796                 }
797                 rogue.playbackMode = false;
798                 rogue.playbackOOS = false;
799 
800                 if(serverMode) {
801                     rogue.nextGame = NG_QUIT;
802                 }
803                 break;
804             case NG_HIGH_SCORES:
805                 rogue.nextGame = NG_NOTHING;
806                 printHighScores(false);
807                 break;
808             case NG_QUIT:
809                 // No need to do anything.
810                 break;
811             default:
812                 break;
813         }
814     } while (rogue.nextGame != NG_QUIT);
815 }
816