1 /*
2 * Copyright (C) 2001-2013 The Exult Team
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17 */
18
19 #ifdef HAVE_CONFIG_H
20 # include <config.h>
21 #endif
22
23 #include <cstring>
24 #include <ctime>
25
26 #include "exult_flx.h"
27
28 #include "Audio.h"
29 #include "Configuration.h"
30 #include "Gump_button.h"
31 #include "Newfile_gump.h"
32 #include "Yesno_gump.h"
33 #include "actors.h"
34 #include "exult.h"
35 #include "game.h"
36 #include "gameclk.h"
37 #include "gamewin.h"
38 #include "listfiles.h"
39 #include "mouse.h"
40 #include "party.h"
41 #include "Text_button.h"
42 #include "miscinf.h"
43 #include "array_size.h"
44 #include "touchui.h"
45 #include "Gump_manager.h"
46
47 using std::atoi;
48 using std::cout;
49 using std::endl;
50 using std::isdigit;
51 using std::memset;
52 using std::qsort;
53 using std::string;
54 using std::strlen;
55 using std::strncpy;
56 using std::strcpy;
57 using std::strcat;
58 using std::time_t;
59 using std::tm;
60 using std::localtime;
61 using std::time;
62
63 /*
64 * Macros:
65 */
66
67 /*
68 * Statics:
69 */
70 // Button Coords
71 const short Newfile_gump::btn_rows[5] = {186, 2, 15, 158, 171};
72 const short Newfile_gump::btn_cols[5] = {2, 46, 88, 150, 209};
73
74 // Text field info
75 const short Newfile_gump::fieldx = 2; // Start Y of each field
76 const short Newfile_gump::fieldy = 2; // Start X of first
77 const short Newfile_gump::fieldw = 207; // Width of each field
78 const short Newfile_gump::fieldh = 12; // Height of each field
79 const short Newfile_gump::fieldgap = 1; // Gap between fields
80 const short Newfile_gump::fieldcount = 14; // Number of fields
81 const short Newfile_gump::textx = 12; // X Offset in field
82 const short Newfile_gump::texty = 2; // Y Offset in field
83 const short Newfile_gump::textw = 190; // Maximum allowable width of text (pixels)
84 const short Newfile_gump::iconx = 2; // X Offset in field
85 const short Newfile_gump::icony = 2; // Y Offset in field
86
87 // Scrollbar and Slider Info
88 const short Newfile_gump::scrollx = 212; // X Offset
89 const short Newfile_gump::scrolly = 28; // Y Offset
90 const short Newfile_gump::scrollh = 129; // Height of Scroll Bar
91 const short Newfile_gump::sliderw = 7; // Width of Slider
92 const short Newfile_gump::sliderh = 7; // Height of Slider
93
94 const short Newfile_gump::infox = 224;
95 const short Newfile_gump::infoy = 67;
96 const short Newfile_gump::infow = 92;
97 const short Newfile_gump::infoh = 79;
98 const char Newfile_gump::infostring[] = "Avatar: %s\n"
99 "Exp: %i Hp: %i\n"
100 "Str: %i Dxt: %i\n"
101 "Int: %i Trn: %i\n"
102 "\n"
103 "Game Day: %i\n"
104 "Game Time: %02i:%02i\n"
105 "\n"
106 "Save Count: %i\n"
107 "Date: %i%s %s %04i\n"
108 "Time: %02i:%02i";
109
110 const char *Newfile_gump::months[12] = {"Jan",
111 "Feb",
112 "March",
113 "April",
114 "May",
115 "June",
116 "July",
117 "Aug",
118 "Sept",
119 "Oct",
120 "Nov",
121 "Dec"
122 };
123
124 static const char *loadtext = "LOAD";
125 static const char *savetext = "SAVE";
126 static const char *deletetext = "DELETE";
127 static const char *canceltext = "CANCEL";
128
129 /*
130 * One of our buttons.
131 */
132 using Newfile_button = CallbackButton<Newfile_gump>;
133 using Newfile_Textbutton = CallbackTextButton<Newfile_gump>;
134
135 /*
136 * Create the load/save box.
137 */
138
Newfile_gump()139 Newfile_gump::Newfile_gump(
140 ) : Modal_gump(nullptr, gwin->get_width() / 2 - 160,
141 gwin->get_height() / 2 - 100,
142 EXULT_FLX_SAVEGUMP_SHP, SF_EXULT_FLX)
143 {
144 set_object_area(TileRect(0, 0, 320, 200), -22, 190); //+++++ ???
145
146 newname[0] = 0;
147
148 gwin->get_tqueue()->pause(SDL_GetTicks());
149 back = gwin->get_win()->create_buffer(gwin->get_width(), gwin->get_height());
150 gwin->get_win()->get(back.get(), 0, 0);
151
152 // Cancel
153 buttons[id_close] = std::make_unique<Newfile_Textbutton>(this, &Newfile_gump::close,
154 canceltext, btn_cols[3], btn_rows[0], 59);
155
156 // Scrollers.
157 buttons[id_page_up] = std::make_unique<Newfile_button>(this, &Newfile_gump::page_up,
158 EXULT_FLX_SAV_UPUP_SHP, btn_cols[4], btn_rows[1], SF_EXULT_FLX);
159 buttons[id_line_up] = std::make_unique<Newfile_button>(this, &Newfile_gump::line_up,
160 EXULT_FLX_SAV_UP_SHP, btn_cols[4], btn_rows[2], SF_EXULT_FLX);
161 buttons[id_line_down] = std::make_unique<Newfile_button>(this, &Newfile_gump::line_down,
162 EXULT_FLX_SAV_DOWN_SHP, btn_cols[4], btn_rows[3], SF_EXULT_FLX);
163 buttons[id_page_down] = std::make_unique<Newfile_button>(this, &Newfile_gump::page_down,
164 EXULT_FLX_SAV_DOWNDOWN_SHP, btn_cols[4], btn_rows[4], SF_EXULT_FLX);
165
166 LoadSaveGameDetails();
167 if (touchui != nullptr) {
168 touchui->hideGameControls();
169 touchui->hideButtonControls();
170 }
171 }
172
173 /*
174 * Delete the load/save box.
175 */
176
~Newfile_gump()177 Newfile_gump::~Newfile_gump(
178 ) {
179 gwin->get_tqueue()->resume(SDL_GetTicks());
180 FreeSaveGameDetails();
181 if (touchui != nullptr) {
182 touchui->showButtonControls();
183 if (!gumpman->gump_mode() || (!gumpman->modal_gump_mode() && gumpman->gumps_dont_pause_game()))
184 touchui->showGameControls();
185 }
186 }
187
188 /*
189 * 'Load' clicked.
190 */
191
load()192 void Newfile_gump::load() {
193 // Shouldn't ever happen.
194 if (selected == -2 || selected == -3)
195 return;
196
197
198 // Aborts if unsuccessful.
199 if (selected != -1) gwin->restore_gamedat(games[selected].num);
200
201 // Read Gamedat
202 gwin->read();
203
204 // Set Done
205 done = true;
206 restored = 1;
207
208 // Reset Selection
209 selected = -3;
210
211 buttons[id_load].reset();
212 buttons[id_save].reset();
213 buttons[id_delete].reset();
214
215 //Reread save game details (quick save gets overwritten)
216 //FreeSaveGameDetails();
217 //LoadSaveGameDetails();
218 //paint();
219 //gwin->set_painted();
220 }
221
222 /*
223 * 'Save' clicked.
224 */
225
save()226 void Newfile_gump::save() {
227 // Shouldn't ever happen.
228 if (!strlen(newname) || selected == -3)
229 return;
230
231
232 // Already a game in this slot? If so ask to delete
233 if (selected != -2) if (!Yesno_gump::ask("Okay to write over existing saved game?"))
234 return;
235
236 // Write to gamedat
237 gwin->write();
238
239 // Now write to savegame file
240 if (selected >= 0) gwin->save_gamedat(games[selected].num, newname);
241 else if (selected == -2) gwin->save_gamedat(first_free, newname);
242
243 cout << "Saved game #" << selected << " successfully." << endl;
244
245 // Reset everything
246 selected = -3;
247
248 buttons[id_load].reset();
249 buttons[id_save].reset();
250 buttons[id_delete].reset();
251
252 FreeSaveGameDetails();
253 LoadSaveGameDetails();
254 paint();
255 gwin->set_painted();
256 gwin->got_bad_feeling(4);
257 }
258
259 /*
260 * 'Delete' clicked.
261 */
262
delete_file()263 void Newfile_gump::delete_file() {
264 // Shouldn't ever happen.
265 if (selected == -1 || selected == -2 || selected == -3)
266 return;
267
268
269 // Ask to delete
270 if (!Yesno_gump::ask("Okay to delete saved game?"))
271 return;
272
273 U7remove(games[selected].filename);
274 filename = nullptr;
275 is_readable = false;
276
277 cout << "Deleted Save game #" << selected << " (" << games[selected].filename << ") successfully." << endl;
278
279 // Reset everything
280 selected = -3;
281
282 buttons[id_load].reset();
283 buttons[id_save].reset();
284 buttons[id_delete].reset();
285
286 FreeSaveGameDetails();
287 LoadSaveGameDetails();
288 paint();
289 gwin->set_painted();
290 }
291
292 /*
293 * Scroll Line
294 */
295
scroll_line(int dir)296 void Newfile_gump::scroll_line(int dir) {
297 list_position += dir;
298
299 if (list_position > num_games - fieldcount)
300 list_position = num_games - fieldcount;
301
302 if (list_position < -2)
303 list_position = -2;
304
305 #ifdef DEBUG
306 cout << "New list position " << list_position << endl;
307 #endif
308
309 paint();
310 gwin->set_painted();
311 }
312
313 /*
314 * Scroll Page
315 */
316
scroll_page(int dir)317 void Newfile_gump::scroll_page(int dir) {
318 scroll_line(dir * fieldcount);
319 }
320
PaintSaveName(int line)321 void Newfile_gump::PaintSaveName(int line) {
322
323 int actual_game = line + list_position;
324
325 if (actual_game < -2 || actual_game >= num_games) return;
326
327 const char *text;
328
329 if (actual_game == -1)
330 text = "Quick Save";
331 else if (actual_game == -2 && selected != -2)
332 text = "Empty Slot";
333 else if (actual_game != selected || buttons[id_load])
334 text = games[actual_game].savename;
335 else
336 text = newname;
337
338 sman->paint_text(2, text,
339 x + fieldx + textx,
340 y + fieldy + texty + line * (fieldh + fieldgap));
341
342 // Being Edited? If so paint cursor
343 if (selected == actual_game && cursor != -1)
344 gwin->get_win()->fill8(0, 1, sman->get_text_height(2),
345 x + fieldx + textx + sman->get_text_width(2, text, cursor),
346 y + fieldy + texty + line * (fieldh + fieldgap));
347
348 // If selected, show selected icon
349 if (selected == actual_game) {
350 ShapeID icon(EXULT_FLX_SAV_SELECTED_SHP, 0, SF_EXULT_FLX);
351 icon.paint_shape(x + fieldx + iconx,
352 y + fieldy + icony + line * (fieldh + fieldgap));
353 }
354 }
355
356 /*
357 * Paint on screen.
358 */
359
paint()360 void Newfile_gump::paint(
361 ) {
362 if (!games)
363 return; // No list, so skip out.
364 Gump::paint();
365
366 // Paint text objects.
367 int i;
368
369 for (i = 0; i < fieldcount; i++)
370 PaintSaveName(i);
371
372 // Paint Buttons
373 for (auto& btn : buttons)
374 if (btn)
375 btn->paint();
376
377 // Paint scroller
378
379 // First thing, work out number of positions that the scroller can be in
380 int num_pos = (2 + num_games) - fieldcount;
381 if (num_pos < 1) num_pos = 1;
382
383 // Now work out the position
384 int pos = ((scrollh - sliderh) * (list_position + 2)) / num_pos;
385
386 ShapeID slider_shape(EXULT_FLX_SAV_SLIDER_SHP, 0, SF_EXULT_FLX);
387 slider_shape.paint_shape(x + scrollx , y + scrolly + pos);
388
389 // Now paint the savegame details
390 if (screenshot)
391 sman->paint_shape(x + 222, y + 2, screenshot->get_frame(0));
392
393 // Need to ensure that the avatar's shape actually exists
394 if (party && !sman->have_si_shapes() &&
395 Shapeinfo_lookup::IsSkinImported(party[0].shape)) {
396 // Female if odd, male if even
397 if (party[0].shape % 2) party[0].shape = Shapeinfo_lookup::GetFemaleAvShape();
398 else party[0].shape = Shapeinfo_lookup::GetMaleAvShape();
399 }
400
401 if (details && party) {
402 int i;
403
404 for (i = 0; i < 4 && i < details->party_size; i++) {
405 ShapeID shape(party[i].shape, 16, static_cast<ShapeFile>(party[i].shape_file));
406 shape.paint_shape(x + 249 + i * 23, y + 169);
407 }
408
409 for (i = 4; i < 8 && i < details->party_size; i++) {
410 ShapeID shape(party[i].shape, 16, static_cast<ShapeFile>(party[i].shape_file));
411 shape.paint_shape(x + 249 + (i - 4) * 23, y + 198);
412 }
413
414 char info[320];
415
416 const char *suffix = "th";
417
418 if ((details->real_day % 10) == 1 && details->real_day != 11)
419 suffix = "st";
420 else if ((details->real_day % 10) == 2 && details->real_day != 12)
421 suffix = "nd";
422 else if ((details->real_day % 10) == 3 && details->real_day != 13)
423 suffix = "rd";
424
425 snprintf(info, sizeof(info), infostring, party[0].name,
426 party[0].exp, party[0].health,
427 party[0].str, party[0].dext,
428 party[0].intel, party[0].training,
429 details->game_day, details->game_hour, details->game_minute,
430 details->save_count,
431 details->real_day, suffix, months[details->real_month - 1], details->real_year,
432 details->real_hour, details->real_minute);
433
434 if (filename) {
435 std::strncat(info, "\nFile: ", sizeof(info) - strlen(info) - 1);
436
437 int offset = strlen(filename);
438
439 while (offset--) {
440 if (filename[offset] == '/' || filename[offset] == '\\') {
441 offset++;
442 break;
443 }
444 }
445 std::strncat(info, filename + offset, sizeof(info) - strlen(info) - 1);
446
447 }
448
449 sman->paint_text_box(4, info, x + infox, y + infoy, infow, infoh);
450
451 } else {
452 if (filename) {
453 char info[64] = "File: ";
454
455 int offset = strlen(filename);
456
457 while (offset--) {
458 if (filename[offset] == '/' || filename[offset] == '\\') {
459 offset++;
460 break;
461 }
462 }
463 std::strncat(info, filename + offset, sizeof(info) - strlen(info) - 1);
464 sman->paint_text_box(4, info, x + infox, y + infoy, infow, infoh);
465
466 }
467
468 if (!is_readable) {
469 sman->paint_text(2, "Unreadable", x + infox + (infow - sman->get_text_width(2, "Unreadable")) / 2, y + infoy + (infoh - 18) / 2);
470 sman->paint_text(2, "Savegame", x + infox + (infow - sman->get_text_width(2, "Savegame")) / 2, y + infoy + (infoh) / 2);
471 } else {
472 sman->paint_text(4, "No Info", x + infox + (infow - sman->get_text_width(4, "No Info")) / 2, y + infoy + (infoh - sman->get_text_height(4)) / 2);
473 }
474 }
475 gwin->set_painted();
476 }
477
478 /*
479 * Handle mouse-down events.
480 */
481
mouse_down(int mx,int my,int button)482 bool Newfile_gump::mouse_down(
483 int mx, int my, int button // Position in window.
484 ) {
485 if (button != 1) return false;
486
487 slide_start = -1;
488
489 pushed = Gump::on_button(mx, my);
490 // Try buttons at bottom.
491 if (!pushed)
492 for (auto& btn : buttons)
493 if (btn && btn->on_button(mx, my)) {
494 pushed = btn.get();
495 break;
496 }
497
498 if (pushed) { // On a button?
499 if (!pushed->push(button)) pushed = nullptr;
500 return true;
501 }
502
503 int gx = mx - x;
504 int gy = my - y;
505
506 // Check for scroller
507 if (gx >= scrollx && gx < scrollx + sliderw && gy >= scrolly && gy < scrolly + scrollh) {
508 int num_pos = (2 + num_games) - fieldcount;
509 if (num_pos < 1) num_pos = 1;
510
511 // Now work out the position
512 int pos = ((scrollh - sliderh) * (list_position + 2)) / num_pos;
513
514 // Pressed above it
515 if (gy < pos + scrolly) {
516 scroll_page(-1);
517 paint();
518 return true;
519 }
520 // Pressed below it
521 else if (gy >= pos + scrolly + sliderh) {
522 scroll_page(1);
523 paint();
524 return true;
525 }
526 // Pressed on it
527 else {
528 slide_start = gy;
529 return true;
530 }
531 }
532
533
534 // Now check for text fields
535 if (gx < fieldx || gx >= fieldx + fieldw)
536 return true;
537
538 int hit = -1;
539 int i;
540 for (i = 0; i < fieldcount; i++) {
541 int fy = fieldy + i * (fieldh + fieldgap);
542 if (gy >= fy && gy < fy + fieldh) {
543 hit = i;
544 break;
545 }
546 }
547
548 if (hit == -1) return true;
549
550 last_selected = selected;
551 if (hit + list_position >= num_games || hit + list_position < -2 || selected == hit + list_position) return true;
552
553 #ifdef DEBUG
554 cout << "Hit a save game field" << endl;
555 #endif
556 selected = hit + list_position;
557
558 bool want_load = true;
559 bool want_delete = true;
560 bool want_save = true;
561
562 if (selected == -2) {
563 want_load = false;
564 want_delete = false;
565 want_save = false;
566 screenshot = cur_shot.get();
567 details = cur_details;
568 party = cur_party;
569 newname[0] = 0;
570 cursor = 0;
571 is_readable = true;
572 filename = nullptr;
573 } else if (selected == -1) {
574 want_delete = false;
575 screenshot = gd_shot.get();
576 details = gd_details;
577 party = gd_party;
578 strcpy(newname, "Quick Save");
579 cursor = -1; // No cursor
580 is_readable = true;
581 filename = nullptr;
582 } else {
583 screenshot = games[selected].screenshot.get();
584 details = games[selected].details;
585 party = games[selected].party;
586 strcpy(newname, games[selected].savename);
587 cursor = static_cast<int>(strlen(newname));
588 is_readable = want_load = games[selected].readable;
589 filename = games[selected].filename;
590 }
591
592 if (!buttons[id_load] && want_load)
593 buttons[id_load] = std::make_unique<Newfile_Textbutton>(this, &Newfile_gump::load,
594 loadtext, btn_cols[1], btn_rows[0], 39);
595 else if (buttons[id_load] && !want_load) {
596 buttons[id_load].reset();
597 }
598
599 if (!buttons[id_save] && want_save)
600 buttons[id_save] = std::make_unique<Newfile_Textbutton>(this, &Newfile_gump::save,
601 savetext, btn_cols[0], btn_rows[0], 40);
602 else if (buttons[id_save] && !want_save) {
603 buttons[id_save].reset();
604 }
605
606 if (!buttons[id_delete] && want_delete)
607 buttons[id_delete] = std::make_unique<Newfile_Textbutton>(this, &Newfile_gump::delete_file,
608 deletetext, btn_cols[2], btn_rows[0], 59);
609 else if (buttons[id_delete] && !want_delete) {
610 buttons[id_delete].reset();
611 }
612
613 paint(); // Repaint.
614 gwin->set_painted();
615 return true;
616 // See if on text field.
617 }
618
619 /*
620 * Handle mouse-up events.
621 */
622
mouse_up(int mx,int my,int button)623 bool Newfile_gump::mouse_up(
624 int mx, int my, int button // Position in window.
625 ) {
626 if (button != 1) return false;
627
628 slide_start = -1;
629
630 if (pushed) { // Pushing a button?
631 pushed->unpush(button);
632 if (pushed->on_button(mx, my))
633 pushed->activate(button);
634 pushed = nullptr;
635 }
636 if (touchui != nullptr && ((selected == -2 && last_selected != -4) || (selected >= 0 && selected == last_selected))) {
637 touchui->promptForName(newname);
638 }
639 //reset so the prompt doesn't pop up on closing
640 last_selected = -4;
641
642 return true;
643 }
644
mousewheel_up()645 void Newfile_gump::mousewheel_up() {
646 SDL_Keymod mod = SDL_GetModState();
647 if (mod & KMOD_ALT)
648 scroll_page(-1);
649 else
650 scroll_line(-1);
651 }
652
mousewheel_down()653 void Newfile_gump::mousewheel_down() {
654 SDL_Keymod mod = SDL_GetModState();
655 if (mod & KMOD_ALT)
656 scroll_page(1);
657 else
658 scroll_line(1);
659 }
660
661 /*
662 * Mouse was dragged with left button down.
663 */
664
mouse_drag(int mx,int my)665 void Newfile_gump::mouse_drag(
666 int mx, int my // Where mouse is.
667 ) {
668 // If not sliding don't do anything
669 if (slide_start == -1) return;
670
671 int gx = mx - x;
672 int gy = my - y;
673
674 // First if the position is too far away from the slider
675 // We'll put it back to the start
676 int sy = gy - scrolly;
677 if (gx < scrollx - 20 || gx > scrollx + sliderw + 20)
678 sy = slide_start - scrolly;
679
680 if (sy < sliderh / 2) sy = sliderh / 2;
681 if (sy > scrollh - sliderh / 2) sy = scrollh - sliderh / 2;
682 sy -= sliderh / 2;
683
684 // Now work out the number of positions
685 int num_pos = (2 + num_games) - fieldcount;
686
687 // Can't scroll if there is less than 1 pos
688 if (num_pos < 1) return;
689
690 // Now work out the closest position to here position
691 int new_pos = ((sy * num_pos * 2) / (scrollh - sliderh) + 1) / 2 - 2;
692
693 if (new_pos != list_position) {
694 list_position = new_pos;
695 paint();
696 }
697 }
698
text_input(const char * text)699 void Newfile_gump::text_input(const char *text) {
700 if (cursor == -1 || strlen(text) >= MAX_SAVEGAME_NAME_LEN - 1)
701 return;
702 if (strcmp(text, newname) == 0) // Not changed
703 return;
704
705 strcpy(newname, text);
706 cursor = static_cast<int>(strlen(text));
707
708 if (newname[id_load] && !buttons[id_save]) {
709 buttons[id_save] = std::make_unique<Newfile_Textbutton>(this, &Newfile_gump::save,
710 savetext, btn_cols[0], btn_rows[0], 40);
711 buttons[id_save]->paint();
712 }
713
714 // Remove Load and Delete Button
715 buttons[id_load].reset();
716 buttons[id_delete].reset();
717
718 screenshot = cur_shot.get();
719 details = cur_details;
720 party = cur_party;
721
722 paint();
723 gwin->set_painted();
724 }
725
726 /*
727 * Handle character that was typed.
728 */
729
text_input(int chr,int unicode)730 void Newfile_gump::text_input(int chr, int unicode) {
731 bool update_details = false;
732 int repaint = false;
733
734 // Are we selected on some text?
735 if (selected == -3)
736 return;
737
738 switch (chr) {
739 case SDLK_RETURN: // If only 'Save', do it.
740 case SDLK_KP_ENTER:
741 if (!buttons[id_load] && buttons[id_save]) {
742 if (buttons[id_save]->push(1)) {
743 gwin->show(true);
744 buttons[id_save]->unpush(1);
745 gwin->show(true);
746 buttons[id_save]->activate(1);
747 }
748 }
749 update_details = true;
750 break;
751
752 case SDLK_BACKSPACE:
753 if (BackspacePressed()) {
754 // Can't restore/delete now.
755 buttons[id_load].reset();
756 buttons[id_delete].reset();
757
758 // If no chars cant save either
759 if (!newname[0]) {
760 buttons[id_save].reset();
761 }
762 update_details = true;
763 }
764 break;
765
766 case SDLK_DELETE:
767 if (DeletePressed()) {
768 // Can't restore/delete now.
769 buttons[id_load].reset();
770 buttons[id_delete].reset();
771
772 // If no chars cant save either
773 if (!newname[0]) {
774 buttons[id_save].reset();
775 }
776 update_details = true;
777 }
778 break;
779
780 case SDLK_LEFT:
781 repaint = MoveCursor(-1);
782 break;
783
784 case SDLK_RIGHT:
785 repaint = MoveCursor(1);
786 break;
787
788 case SDLK_HOME:
789 repaint = MoveCursor(-MAX_SAVEGAME_NAME_LEN);
790 break;
791
792 case SDLK_END:
793 repaint = MoveCursor(MAX_SAVEGAME_NAME_LEN);
794 break;
795
796 default:
797 ignore_unused_variable_warning(unicode);
798 if (chr < ' ')
799 return; // Ignore other special chars.
800
801 if (chr < 256 && isascii(chr)) {
802 if (AddCharacter(chr)) {
803 // Added first character? Need 'Save' button.
804 if (newname[0] && !buttons[id_save]) {
805 buttons[id_save] = std::make_unique<Newfile_Textbutton>(this, &Newfile_gump::save,
806 savetext, btn_cols[0], btn_rows[0], 40);
807 buttons[id_save]->paint();
808 }
809
810 // Remove Load and Delete Button
811 if (buttons[id_load] || buttons[id_delete]) {
812 buttons[id_load].reset();
813 buttons[id_delete].reset();
814 }
815 update_details = true;
816 }
817 }
818 break;
819 }
820
821 // This sets the game details to the cur set
822 if (update_details) {
823 screenshot = cur_shot.get();
824 details = cur_details;
825 party = cur_party;
826 repaint = true;
827 }
828 if (repaint) {
829 paint();
830 gwin->set_painted();
831 }
832 }
833
BackspacePressed()834 int Newfile_gump::BackspacePressed() {
835 if (cursor == -1 || cursor == 0) return 0;
836 cursor--;
837 return DeletePressed();
838 }
DeletePressed()839 int Newfile_gump::DeletePressed() {
840 if (cursor == -1 || cursor == static_cast<int>(strlen(newname)))
841 return 0;
842 for (unsigned i = cursor; i < strlen(newname); i++)
843 newname[i] = newname[i + 1];
844
845 return 1;
846 }
MoveCursor(int count)847 int Newfile_gump::MoveCursor(int count) {
848 if (cursor == -1) return 0;
849
850 cursor += count;
851 if (cursor < 0) cursor = 0;
852 if (cursor > static_cast<int>(strlen(newname))) cursor = strlen(newname);
853
854 return 1;
855 }
AddCharacter(char c)856 int Newfile_gump::AddCharacter(char c) {
857 if (cursor == -1 || cursor == MAX_SAVEGAME_NAME_LEN - 1) return 0;
858
859 char text[MAX_SAVEGAME_NAME_LEN];
860
861 strcpy(text, newname);
862 text[cursor + 1] = 0;
863 text[cursor] = c;
864 strcat(text, newname + cursor);
865
866 //Now check the width of the text
867 if (sman->get_text_width(2, text) >= textw)
868 return 0;
869
870 cursor++;
871 strcpy(newname, text);
872 return 1;
873 }
874
LoadSaveGameDetails()875 void Newfile_gump::LoadSaveGameDetails() {
876 int i;
877
878 // Gamedat Details
879 gwin->get_saveinfo(gd_shot, gd_details, gd_party);
880
881 // Current screenshot
882 cur_shot = gwin->create_mini_screenshot();
883
884 // Current Details
885 cur_details = new SaveGame_Details;
886 memset(cur_details, 0, sizeof(SaveGame_Details));
887
888 gwin->get_win()->put(back.get(), 0, 0);
889
890 if (gd_details) cur_details->save_count = gd_details->save_count;
891 else cur_details->save_count = 0;
892
893 cur_details->party_size = partyman->get_count() + 1;
894 cur_details->game_day = static_cast<short>(gclock->get_total_hours() / 24);
895 cur_details->game_hour = gclock->get_hour();
896 cur_details->game_minute = gclock->get_minute();
897
898 time_t t = time(nullptr);
899 tm *timeinfo = localtime(&t);
900
901 cur_details->real_day = timeinfo->tm_mday;
902 cur_details->real_hour = timeinfo->tm_hour;
903 cur_details->real_minute = timeinfo->tm_min;
904 cur_details->real_month = timeinfo->tm_mon + 1;
905 cur_details->real_year = timeinfo->tm_year + 1900;
906 cur_details->real_second = timeinfo->tm_sec;
907
908 // Current Party
909 cur_party = new SaveGame_Party[cur_details->party_size];
910 for (i = 0; i < cur_details->party_size ; i++) {
911 Actor *npc;
912 if (i == 0)
913 npc = gwin->get_main_actor();
914 else
915 npc = gwin->get_npc(partyman->get_member(i - 1));
916
917 std::string namestr = npc->get_npc_name();
918 strncpy(cur_party[i].name, namestr.c_str(), 18);
919 cur_party[i].shape = npc->get_shapenum();
920 cur_party[i].shape_file = npc->get_shapefile();
921
922 cur_party[i].dext = npc->get_property(Actor::dexterity);
923 cur_party[i].str = npc->get_property(Actor::strength);
924 cur_party[i].intel = npc->get_property(Actor::intelligence);
925 cur_party[i].health = npc->get_property(Actor::health);
926 cur_party[i].combat = npc->get_property(Actor::combat);
927 cur_party[i].mana = npc->get_property(Actor::mana);
928 cur_party[i].magic = npc->get_property(Actor::magic);
929 cur_party[i].training = npc->get_property(Actor::training);
930 cur_party[i].exp = npc->get_property(Actor::exp);
931 cur_party[i].food = npc->get_property(Actor::food_level);
932 cur_party[i].flags = npc->get_flags();
933 cur_party[i].flags2 = npc->get_flags2();
934 }
935
936 party = cur_party;
937 screenshot = cur_shot.get();
938 details = cur_details;
939
940 // Now read save game details
941 char mask[256];
942
943 snprintf(mask, 256, SAVENAME2, GAME_BG ? "bg" : GAME_SI ? "si" : "dev");
944
945 FileList filenames;
946 U7ListFiles(mask, filenames);
947 num_games = filenames.size();
948
949 games = new SaveInfo[num_games];
950
951 // Setup basic details
952 for (i = 0; i < num_games; i++) {
953 games[i].filename = new char[filenames[i].length() + 1];
954 strcpy(games[i].filename, filenames[i].c_str());
955 games[i].SetSeqNumber();
956 }
957
958 // First sort thet games so the will be sorted by number
959 // This is so I can work out the first free game
960 if (num_games)
961 qsort(games, num_games, sizeof(SaveInfo), SaveInfo::CompareGames);
962
963 // Reand and cache all details
964 first_free = -1;
965 for (i = 0; i < num_games; i++) {
966 games[i].readable = gwin->get_saveinfo(games[i].num, games[i].savename, games[i].screenshot,
967 games[i].details, games[i].party);
968
969 if (first_free == -1 && i != games[i].num) first_free = i;
970 }
971
972 if (first_free == -1) first_free = num_games;
973
974 // Now sort it again, with all the details so it can be done by date
975 if (num_games) qsort(games, num_games, sizeof(SaveInfo), SaveInfo::CompareGames);
976
977
978
979 // We'll now output the info if debugging
980 #ifdef DEBUG
981 cout << "Listing " << num_games << " Save games" << endl;
982 for (i = 0; i < num_games; i++)
983 cout << i << " = " << games[i].num << " : " << games[i].filename << " : " << games[i].savename << endl;
984
985 cout << "First Free Game " << first_free << endl;
986 #endif
987 }
988
FreeSaveGameDetails()989 void Newfile_gump::FreeSaveGameDetails() {
990 cur_shot.reset();
991 delete cur_details;
992 cur_details = nullptr;
993 delete [] cur_party;
994 cur_party = nullptr;
995
996 gd_shot.reset();
997 delete gd_details;
998 gd_details = nullptr;
999 delete [] gd_party;
1000 gd_party = nullptr;
1001
1002 filename = nullptr;
1003
1004 // The SaveInfo struct will delete everything that it's got allocated
1005 // So we don't need to worry about that
1006 delete [] games;
1007 games = nullptr;
1008 }
1009
1010 // Save Info Methods
1011
1012 // Destructor
~SaveInfo()1013 Newfile_gump::SaveInfo::~SaveInfo() {
1014 delete [] filename;
1015 delete [] savename;
1016 delete details;
1017 delete [] party;
1018 }
1019
1020 // Set Sequence Number
SetSeqNumber()1021 void Newfile_gump::SaveInfo::SetSeqNumber() {
1022 int i;
1023
1024 for (i = strlen(filename) - 1; !isdigit(static_cast<unsigned char>(filename[i])); i--)
1025 ;
1026 for (; isdigit(static_cast<unsigned char>(filename[i])); i--)
1027 ;
1028
1029 num = atoi(filename + i + 1);
1030 }
1031
1032 // Compare This
CompareThis(const SaveInfo * other) const1033 int Newfile_gump::SaveInfo::CompareThis(const SaveInfo *other) const {
1034 // Check by time first, if possible
1035 if (details && other->details) {
1036 if (details->real_year < other->details->real_year)
1037 return 1;
1038 if (details->real_year > other->details->real_year)
1039 return -1;
1040
1041 if (details->real_month < other->details->real_month)
1042 return 1;
1043 if (details->real_month > other->details->real_month)
1044 return -1;
1045
1046 if (details->real_day < other->details->real_day)
1047 return 1;
1048 if (details->real_day > other->details->real_day)
1049 return -1;
1050
1051 if (details->real_hour < other->details->real_hour)
1052 return 1;
1053 if (details->real_hour > other->details->real_hour)
1054 return -1;
1055
1056 if (details->real_minute < other->details->real_minute)
1057 return 1;
1058 if (details->real_minute > other->details->real_minute)
1059 return -1;
1060
1061 if (details->real_second < other->details->real_second)
1062 return 1;
1063 if (details->real_second > other->details->real_second)
1064 return -1;
1065 } else if (details) { // If the other doesn't have time we are first
1066 return -1;
1067 } else if (other->details) { // If we don't have time we are last
1068 return 1;
1069 }
1070
1071 return num - other->num;
1072 }
1073
1074 // Compare Games Static
CompareGames(const void * a,const void * b)1075 int Newfile_gump::SaveInfo::CompareGames(const void *a, const void *b) {
1076 return static_cast<Newfile_gump::SaveInfo const *>(a)->CompareThis(static_cast<Newfile_gump::SaveInfo const *>(b));
1077 }
1078