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