1 /*
2
3 Copyright (C) 1991-2001 and beyond by Bungie Studios, Inc.
4 and the "Aleph One" developers.
5
6 This program is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 3 of the License, or
9 (at your option) any later version.
10
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 This license is contained in the file "COPYING",
17 which is included with this source code; it is available online at
18 http://www.gnu.org/licenses/gpl.html
19
20 */
21
22 /*
23 * FileHandler_SDL.cpp - Platform-independant file handling, SDL implementation
24 *
25 * Written in 2000 by Christian Bauer
26 */
27 #include "cseries.h"
28 #include "FileHandler.h"
29 #include "resource_manager.h"
30
31 #include "shell.h"
32 #include "interface.h"
33 #include "game_errors.h"
34 #include "tags.h"
35
36 #include <stdio.h>
37 #include <stdlib.h>
38 #include <errno.h>
39 #include <limits.h>
40 #include <string>
41 #include <vector>
42
43 #include <SDL_endian.h>
44
45 #ifdef HAVE_UNISTD_H
46 #include <sys/stat.h>
47 #include <fcntl.h>
48 #include <dirent.h>
49 #include <unistd.h>
50 #endif
51
52 #ifdef HAVE_ZZIP
53 #include <zzip/lib.h>
54 #include "SDL_rwops_zzip.h"
55 #endif
56
57 #if defined(__WIN32__)
58 #define PATH_SEP '\\'
59 #else
60 #define PATH_SEP '/'
61 #endif
62
63 #include "sdl_dialogs.h"
64 #include "sdl_widgets.h"
65 #include "SoundManager.h" // !
66
67 #include "preferences.h"
68
69 #include <boost/bind.hpp>
70 #include <boost/function.hpp>
71 #include <boost/algorithm/string/predicate.hpp>
72 #include <boost/filesystem.hpp>
73
74 namespace io = boost::iostreams;
75
76 // From shell_sdl.cpp
77 extern vector<DirectorySpecifier> data_search_path;
78 extern DirectorySpecifier local_data_dir, preferences_dir, saved_games_dir, quick_saves_dir, image_cache_dir, recordings_dir;
79
80 extern bool is_applesingle(SDL_RWops *f, bool rsrc_fork, int32 &offset, int32 &length);
81 extern bool is_macbinary(SDL_RWops *f, int32 &data_length, int32 &rsrc_length);
82
83 /*
84 * Opened file
85 */
86
OpenedFile()87 OpenedFile::OpenedFile() : f(NULL), err(0), is_forked(false), fork_offset(0), fork_length(0) {}
88
IsOpen()89 bool OpenedFile::IsOpen()
90 {
91 return f != NULL;
92 }
93
Close()94 bool OpenedFile::Close()
95 {
96 if (f) {
97 SDL_RWclose(f);
98 f = NULL;
99 err = 0;
100 }
101 is_forked = false;
102 fork_offset = 0;
103 fork_length = 0;
104 return true;
105 }
106
GetPosition(int32 & Position)107 bool OpenedFile::GetPosition(int32 &Position)
108 {
109 if (f == NULL)
110 return false;
111
112 err = 0;
113 Position = SDL_RWtell(f) - fork_offset;
114 return true;
115 }
116
SetPosition(int32 Position)117 bool OpenedFile::SetPosition(int32 Position)
118 {
119 if (f == NULL)
120 return false;
121
122 err = 0;
123 if (SDL_RWseek(f, Position + fork_offset, SEEK_SET) < 0)
124 err = errno;
125 return err == 0;
126 }
127
GetLength(int32 & Length)128 bool OpenedFile::GetLength(int32 &Length)
129 {
130 if (f == NULL)
131 return false;
132
133 if (is_forked)
134 Length = fork_length;
135 else {
136 int32 pos = SDL_RWtell(f);
137 SDL_RWseek(f, 0, SEEK_END);
138 Length = SDL_RWtell(f);
139 SDL_RWseek(f, pos, SEEK_SET);
140 }
141 err = 0;
142 return true;
143 }
144
Read(int32 Count,void * Buffer)145 bool OpenedFile::Read(int32 Count, void *Buffer)
146 {
147 if (f == NULL)
148 return false;
149
150 err = 0;
151 return (SDL_RWread(f, Buffer, 1, Count) == Count);
152 }
153
Write(int32 Count,void * Buffer)154 bool OpenedFile::Write(int32 Count, void *Buffer)
155 {
156 if (f == NULL)
157 return false;
158
159 err = 0;
160 return (SDL_RWwrite(f, Buffer, 1, Count) == Count);
161 }
162
163
TakeRWops()164 SDL_RWops *OpenedFile::TakeRWops ()
165 {
166 SDL_RWops *taken = f;
167 f = NULL;
168 Close ();
169 return taken;
170 }
171
opened_file_device(OpenedFile & f)172 opened_file_device::opened_file_device(OpenedFile& f) : f(f) { }
173
read(char * s,std::streamsize n)174 std::streamsize opened_file_device::read(char* s, std::streamsize n)
175 {
176 return SDL_RWread(f.GetRWops(), s, 1, n);
177 }
178
write(const char * s,std::streamsize n)179 std::streamsize opened_file_device::write(const char* s, std::streamsize n)
180 {
181 return SDL_RWwrite(f.GetRWops(), s, 1, n);
182 }
183
seek(io::stream_offset off,std::ios_base::seekdir way)184 std::streampos opened_file_device::seek(io::stream_offset off, std::ios_base::seekdir way)
185 {
186 std::streampos pos;
187
188 switch (way)
189 {
190 case std::ios_base::beg:
191 pos = SDL_RWseek(f.GetRWops(), off + f.fork_offset, SEEK_SET);
192 break;
193 case std::ios_base::end:
194 pos = SDL_RWseek(f.GetRWops(), off, SEEK_END);
195 break;
196 case std::ios_base::cur:
197 pos = SDL_RWseek(f.GetRWops(), off, SEEK_CUR);
198 break;
199 default:
200 break;
201 }
202
203 return pos - static_cast<std::streampos>(f.fork_offset);
204 }
205
206 /*
207 * Loaded resource
208 */
209
LoadedResource()210 LoadedResource::LoadedResource() : p(NULL), size(0) {}
211
IsLoaded()212 bool LoadedResource::IsLoaded()
213 {
214 return p != NULL;
215 }
216
Unload()217 void LoadedResource::Unload()
218 {
219 if (p) {
220 free(p);
221 p = NULL;
222 size = 0;
223 }
224 }
225
GetLength()226 size_t LoadedResource::GetLength()
227 {
228 return size;
229 }
230
GetPointer(bool DoDetach)231 void *LoadedResource::GetPointer(bool DoDetach)
232 {
233 void *ret = p;
234 if (DoDetach)
235 Detach();
236 return ret;
237 }
238
SetData(void * data,size_t length)239 void LoadedResource::SetData(void *data, size_t length)
240 {
241 Unload();
242 p = data;
243 size = length;
244 }
245
Detach()246 void LoadedResource::Detach()
247 {
248 p = NULL;
249 size = 0;
250 }
251
252
253 /*
254 * Opened resource file
255 */
256
OpenedResourceFile()257 OpenedResourceFile::OpenedResourceFile() : f(NULL), saved_f(NULL), err(0) {}
258
Push()259 bool OpenedResourceFile::Push()
260 {
261 saved_f = cur_res_file();
262 if (saved_f != f)
263 use_res_file(f);
264 err = 0;
265 return true;
266 }
267
Pop()268 bool OpenedResourceFile::Pop()
269 {
270 if (f != saved_f)
271 use_res_file(saved_f);
272 err = 0;
273 return true;
274 }
275
Check(uint32 Type,int16 ID)276 bool OpenedResourceFile::Check(uint32 Type, int16 ID)
277 {
278 Push();
279 bool result = has_1_resource(Type, ID);
280 err = result ? 0 : errno;
281 Pop();
282 return result;
283 }
284
Get(uint32 Type,int16 ID,LoadedResource & Rsrc)285 bool OpenedResourceFile::Get(uint32 Type, int16 ID, LoadedResource &Rsrc)
286 {
287 Push();
288 bool success = get_1_resource(Type, ID, Rsrc);
289 err = success ? 0 : errno;
290 Pop();
291 return success;
292 }
293
IsOpen()294 bool OpenedResourceFile::IsOpen()
295 {
296 return f != NULL;
297 }
298
Close()299 bool OpenedResourceFile::Close()
300 {
301 if (f) {
302 close_res_file(f);
303 f = NULL;
304 err = 0;
305 }
306 return true;
307 }
308
309
310 /*
311 * File specification
312 */
313 //AS: Constructor moved here to fix linking errors
FileSpecifier()314 FileSpecifier::FileSpecifier(): err(0) {}
operator =(const FileSpecifier & other)315 const FileSpecifier &FileSpecifier::operator=(const FileSpecifier &other)
316 {
317 if (this != &other) {
318 name = other.name;
319 err = other.err;
320 }
321 return *this;
322 }
323
324 // Create file
Create(Typecode Type)325 bool FileSpecifier::Create(Typecode Type)
326 {
327 Delete();
328 // files are automatically created when opened for writing
329 err = 0;
330 return true;
331 }
332
333 // Create directory
CreateDirectory()334 bool FileSpecifier::CreateDirectory()
335 {
336 err = 0;
337 #if defined(__WIN32__)
338 if (mkdir(GetPath()) < 0)
339 #else
340 if (mkdir(GetPath(), 0777) < 0)
341 #endif
342 err = errno;
343 return err == 0;
344 }
345
346 #ifdef HAVE_ZZIP
unix_path_separators(const std::string & input)347 static std::string unix_path_separators(const std::string& input)
348 {
349 if (PATH_SEP == '/') return input;
350
351 std::string output;
352 for (std::string::const_iterator it = input.begin(); it != input.end(); ++it) {
353 if (*it == PATH_SEP)
354 output.push_back('/');
355 else
356 output.push_back(*it);
357 }
358
359 return output;
360 }
361 #endif
362
363 // Open data file
Open(OpenedFile & OFile,bool Writable)364 bool FileSpecifier::Open(OpenedFile &OFile, bool Writable)
365 {
366 OFile.Close();
367
368 SDL_RWops *f;
369 {
370 #ifdef HAVE_ZZIP
371 if (!Writable)
372 {
373 f = OFile.f = SDL_RWFromZZIP(unix_path_separators(GetPath()).c_str(), "rb");
374 }
375 else {
376 f = OFile.f = SDL_RWFromFile(GetPath(), "wb+");
377 }
378 #else
379 f = OFile.f = SDL_RWFromFile(GetPath(), Writable ? "wb+" : "rb");
380 #endif
381
382 }
383
384 err = f ? 0 : errno;
385 if (f == NULL) {
386 set_game_error(systemError, err);
387 return false;
388 }
389 if (Writable)
390 return true;
391
392 // Transparently handle AppleSingle and MacBinary files on reading
393 int32 offset, data_length, rsrc_length;
394 if (is_applesingle(f, false, offset, data_length)) {
395 OFile.is_forked = true;
396 OFile.fork_offset = offset;
397 OFile.fork_length = data_length;
398 SDL_RWseek(f, offset, SEEK_SET);
399 return true;
400 } else if (is_macbinary(f, data_length, rsrc_length)) {
401 OFile.is_forked = true;
402 OFile.fork_offset = 128;
403 OFile.fork_length = data_length;
404 SDL_RWseek(f, 128, SEEK_SET);
405 return true;
406 }
407 SDL_RWseek(f, 0, SEEK_SET);
408 return true;
409 }
410
411 // Open resource file
Open(OpenedResourceFile & OFile,bool Writable)412 bool FileSpecifier::Open(OpenedResourceFile &OFile, bool Writable)
413 {
414 OFile.Close();
415
416 OFile.f = open_res_file(*this);
417 err = OFile.f ? 0 : errno;
418 if (OFile.f == NULL) {
419 set_game_error(systemError, err);
420 return false;
421 } else
422 return true;
423 }
424
425 // Check for existence of file
Exists()426 bool FileSpecifier::Exists()
427 {
428 // Check whether the file is readable
429 err = 0;
430 if (access(GetPath(), R_OK) < 0)
431 err = errno;
432
433 #ifdef HAVE_ZZIP
434 if (err)
435 {
436 // Check whether zzip can open the file (slow!)
437 ZZIP_FILE* file = zzip_open(unix_path_separators(GetPath()).c_str(), R_OK);
438 if (file)
439 {
440 zzip_close(file);
441 return true;
442 }
443 else
444 {
445 return false;
446 }
447 }
448 #endif
449 return (err == 0);
450 }
451
IsDir()452 bool FileSpecifier::IsDir()
453 {
454 struct stat st;
455 err = 0;
456 if (stat(GetPath(), &st) < 0)
457 return false;
458 return (S_ISDIR(st.st_mode));
459 }
460
461 // Get modification date
GetDate()462 TimeType FileSpecifier::GetDate()
463 {
464 struct stat st;
465 err = 0;
466 if (stat(GetPath(), &st) < 0) {
467 err = errno;
468 return 0;
469 }
470 return st.st_mtime;
471 }
472
473 static const char * alephone_extensions[] = {
474 ".sceA",
475 ".sgaA",
476 ".filA",
477 ".phyA",
478 ".shpA",
479 ".sndA",
480 0
481 };
482
HideExtension(const std::string & filename)483 std::string FileSpecifier::HideExtension(const std::string& filename)
484 {
485 if (environment_preferences->hide_extensions)
486 {
487 const char **extension = alephone_extensions;
488 while (*extension)
489 {
490 if (boost::algorithm::ends_with(filename, *extension))
491 {
492 return filename.substr(0, filename.length() - strlen(*extension));
493 }
494
495 ++extension;
496 }
497 }
498
499 return filename;
500 }
501
502 struct extension_mapping
503 {
504 const char *extension;
505 bool case_sensitive;
506 Typecode typecode;
507 };
508
509 static extension_mapping extensions[] =
510 {
511 // some common extensions, to speed up building map lists
512 { "dds", false, _typecode_unknown },
513 { "jpg", false, _typecode_unknown },
514 { "png", false, _typecode_unknown },
515 { "bmp", false, _typecode_unknown },
516 { "txt", false, _typecode_unknown },
517 { "ttf", false, _typecode_unknown },
518
519 { "lua", false, _typecode_netscript }, // netscript, or unknown?
520 { "mml", false, _typecode_unknown }, // no type code for this yet
521
522 { "sceA", false, _typecode_scenario },
523 { "sgaA", false, _typecode_savegame },
524 { "filA", false, _typecode_film },
525 { "phyA", false, _typecode_physics },
526 { "ShPa", true, _typecode_shapespatch }, // must come before shpA
527 { "shpA", false, _typecode_shapes },
528 { "sndA", false, _typecode_sounds },
529
530 { "scen", false, _typecode_scenario },
531 { "shps", false, _typecode_shapes },
532 { "phys", false, _typecode_physics },
533 { "sndz", false, _typecode_sounds },
534
535 { "mpg", false, _typecode_movie },
536
537 {0, false, _typecode_unknown}
538 };
539
540 // Determine file type
GetType()541 Typecode FileSpecifier::GetType()
542 {
543
544 // if there's an extension, assume it's correct
545 const char *extension = strrchr(GetPath(), '.');
546 if (extension) {
547 extension_mapping *mapping = extensions;
548 while (mapping->extension)
549 {
550 if (( mapping->case_sensitive && (strcmp(extension + 1, mapping->extension) == 0)) ||
551 (!mapping->case_sensitive && (strcasecmp(extension + 1, mapping->extension) == 0)))
552 {
553 return mapping->typecode;
554 }
555 ++mapping;
556 }
557 }
558
559 // Open file
560 OpenedFile f;
561 if (!Open(f))
562 return _typecode_unknown;
563 SDL_RWops *p = f.GetRWops();
564 int32 file_length = 0;
565 f.GetLength(file_length);
566
567 // Check for Sounds file
568 {
569 f.SetPosition(0);
570 uint32 version = SDL_ReadBE32(p);
571 uint32 tag = SDL_ReadBE32(p);
572 if ((version == 0 || version == 1) && tag == FOUR_CHARS_TO_INT('s', 'n', 'd', '2'))
573 return _typecode_sounds;
574 }
575
576 // Check for Map/Physics file
577 {
578 f.SetPosition(0);
579 int version = SDL_ReadBE16(p);
580 int data_version = SDL_ReadBE16(p);
581 if ((version == 0 || version == 1 || version == 2 || version == 4) && (data_version == 0 || data_version == 1 || data_version == 2)) {
582 SDL_RWseek(p, 68, SEEK_CUR);
583 int32 directory_offset = SDL_ReadBE32(p);
584 if (directory_offset >= file_length)
585 goto not_map;
586 f.SetPosition(128);
587 uint32 tag = SDL_ReadBE32(p);
588 // ghs: I do not believe this list is comprehensive
589 // I think it's just what we've seen so far?
590 switch (tag) {
591 case LINE_TAG:
592 case POINT_TAG:
593 case SIDE_TAG:
594 return _typecode_scenario;
595 break;
596 case MONSTER_PHYSICS_TAG:
597 return _typecode_physics;
598 break;
599 }
600
601 }
602 not_map: ;
603 }
604
605 // Check for Shapes file
606 {
607 f.SetPosition(0);
608 for (int i=0; i<32; i++) {
609 uint32 status_flags = SDL_ReadBE32(p);
610 int32 offset = SDL_ReadBE32(p);
611 int32 length = SDL_ReadBE32(p);
612 int32 offset16 = SDL_ReadBE32(p);
613 int32 length16 = SDL_ReadBE32(p);
614 if (status_flags != 0
615 || (offset != NONE && (offset >= file_length || offset + length > file_length))
616 || (offset16 != NONE && (offset16 >= file_length || offset16 + length16 > file_length)))
617 goto not_shapes;
618 SDL_RWseek(p, 12, SEEK_CUR);
619 }
620 return _typecode_shapes;
621 not_shapes: ;
622 }
623
624 // Not identified
625 return _typecode_unknown;
626 }
627
628 // Get free space on disk
GetFreeSpace(uint32 & FreeSpace)629 bool FileSpecifier::GetFreeSpace(uint32 &FreeSpace)
630 {
631 // This is impossible to do in a platform-independant way, so we
632 // just return 16MB which should be enough for everything
633 FreeSpace = 16 * 1024 * 1024;
634 err = 0;
635 return true;
636 }
637
638 // Delete file
Delete()639 bool FileSpecifier::Delete()
640 {
641 err = 0;
642 if (remove(GetPath()) < 0)
643 err = errno;
644 return err == 0;
645 }
646
Rename(const FileSpecifier & Destination)647 bool FileSpecifier::Rename(const FileSpecifier& Destination)
648 {
649 #ifdef WIN32
650 // Work around Windows' non-POSIX behavior on rename().
651 // If we fail, go ahead and try to rename anyway.
652 FileSpecifier d2 = Destination;
653 if (d2.Exists() && !d2.IsDir())
654 d2.Delete();
655 #endif
656 return rename(GetPath(), Destination.GetPath()) == 0;
657 }
658
659 // Set to local (per-user) data directory
SetToLocalDataDir()660 void FileSpecifier::SetToLocalDataDir()
661 {
662 name = local_data_dir.name;
663 }
664
665 // Set to preferences directory
SetToPreferencesDir()666 void FileSpecifier::SetToPreferencesDir()
667 {
668 name = preferences_dir.name;
669 }
670
671 // Set to saved games directory
SetToSavedGamesDir()672 void FileSpecifier::SetToSavedGamesDir()
673 {
674 name = saved_games_dir.name;
675 }
676
677 // Set to newer saved games directory
SetToQuickSavesDir()678 void FileSpecifier::SetToQuickSavesDir()
679 {
680 name = quick_saves_dir.name;
681 }
682
683 // Set to image cache directory
SetToImageCacheDir()684 void FileSpecifier::SetToImageCacheDir()
685 {
686 name = image_cache_dir.name;
687 }
688
689 // Set to recordings directory
SetToRecordingsDir()690 void FileSpecifier::SetToRecordingsDir()
691 {
692 name = recordings_dir.name;
693 }
694
local_path_separators(const char * path)695 static string local_path_separators(const char *path)
696 {
697 string local_path = path;
698 if (PATH_SEP == '/') return local_path;
699
700 for (size_t k = 0; k < local_path.size(); ++k) {
701 if (local_path[k] == '/')
702 local_path[k] = PATH_SEP;
703 }
704
705 return local_path;
706 }
707
708 // Traverse search path, look for file given relative path name
SetNameWithPath(const char * NameWithPath)709 bool FileSpecifier::SetNameWithPath(const char *NameWithPath)
710 {
711 if (*NameWithPath == '\0') {
712 err = ENOENT;
713 return false;
714 }
715
716 FileSpecifier full_path;
717 string rel_path = local_path_separators(NameWithPath);
718
719 vector<DirectorySpecifier>::const_iterator i = data_search_path.begin(), end = data_search_path.end();
720 while (i != end) {
721 full_path = *i + rel_path;
722 if (full_path.Exists()) {
723 name = full_path.name;
724 err = 0;
725 return true;
726 }
727 i++;
728 }
729 err = ENOENT;
730 return false;
731 }
732
SetNameWithPath(const char * NameWithPath,const DirectorySpecifier & Directory)733 bool FileSpecifier::SetNameWithPath(const char* NameWithPath, const DirectorySpecifier& Directory)
734 {
735 if (*NameWithPath == '\0') {
736 err = ENOENT;
737 return false;
738 }
739
740 FileSpecifier full_path;
741 string rel_path = local_path_separators(NameWithPath);
742
743 full_path = Directory + rel_path;
744 if (full_path.Exists()) {
745 name = full_path.name;
746 err = 0;
747 return true;
748 }
749
750 err = ENOENT;
751 return false;
752 }
753
SetTempName(const FileSpecifier & other)754 void FileSpecifier::SetTempName(const FileSpecifier& other)
755 {
756 name = boost::filesystem::unique_path(other.name + "%%%%%%").string();
757 }
758
759 // Get last element of path
GetName(char * part) const760 void FileSpecifier::GetName(char *part) const
761 {
762 string::size_type pos = name.rfind(PATH_SEP);
763 if (pos == string::npos)
764 strcpy(part, name.c_str());
765 else
766 strcpy(part, name.substr(pos + 1).c_str());
767 }
768
769 // Add part to path name
AddPart(const string & part)770 void FileSpecifier::AddPart(const string &part)
771 {
772 if (name.length() && name[name.length() - 1] == PATH_SEP)
773 name += local_path_separators(part.c_str());
774 else
775 name = name + PATH_SEP + local_path_separators(part.c_str());
776
777 canonicalize_path();
778 }
779
780 // Split path to base and last part
SplitPath(string & base,string & part) const781 void FileSpecifier::SplitPath(string &base, string &part) const
782 {
783 string::size_type pos = name.rfind(PATH_SEP);
784 if (pos == string::npos) {
785 base = name;
786 part.erase();
787 } else if (pos == 0) {
788 base = PATH_SEP;
789 part = name.substr(1);
790 } else {
791 base = name.substr(0, pos);
792 part = name.substr(pos + 1);
793 }
794 }
795
796 // Fill file specifier with base name
ToDirectory(DirectorySpecifier & dir)797 void FileSpecifier::ToDirectory(DirectorySpecifier &dir)
798 {
799 string part;
800 SplitPath(dir, part);
801 }
802
803 // Set file specifier from directory specifier
FromDirectory(DirectorySpecifier & Dir)804 void FileSpecifier::FromDirectory(DirectorySpecifier &Dir)
805 {
806 name = Dir.name;
807 }
808
809 // Canonicalize path
canonicalize_path(void)810 void FileSpecifier::canonicalize_path(void)
811 {
812 #if !defined(__WIN32__)
813
814 // Replace multiple consecutive '/'s by a single '/'
815 while (true) {
816 string::size_type pos = name.find("//");
817 if (pos == string::npos)
818 break;
819 name.erase(pos, 1);
820 }
821
822 #endif
823
824 // Remove trailing '/'
825 // ZZZ: only if we're not naming the root directory /
826 if (!name.empty() && name[name.size()-1] == PATH_SEP && name.size() != 1)
827 name.erase(name.size()-1, 1);
828 }
829
830 // Read directory contents
ReadDirectory(vector<dir_entry> & vec)831 bool FileSpecifier::ReadDirectory(vector<dir_entry> &vec)
832 {
833 vec.clear();
834
835 DIR *d = opendir(GetPath());
836
837 if (d == NULL) {
838 err = errno;
839 return false;
840 }
841 struct dirent *de = readdir(d);
842 while (de) {
843 FileSpecifier full_path = name;
844 full_path += de->d_name;
845 struct stat st;
846 if (stat(full_path.GetPath(), &st) == 0) {
847 // Ignore files starting with '.' and the directories '.' and '..'
848 if (de->d_name[0] != '.' || (S_ISDIR(st.st_mode) && !(de->d_name[1] == '\0' || de->d_name[1] == '.')))
849 vec.push_back(dir_entry(de->d_name, st.st_size, S_ISDIR(st.st_mode), false, st.st_mtime));
850 }
851 de = readdir(d);
852 }
853 closedir(d);
854 err = 0;
855 return true;
856 }
857
858 // Copy file contents
CopyContents(FileSpecifier & source_name)859 bool FileSpecifier::CopyContents(FileSpecifier &source_name)
860 {
861 err = 0;
862 OpenedFile src, dst;
863 if (source_name.Open(src)) {
864 Delete();
865 if (Open(dst, true)) {
866 const int BUFFER_SIZE = 1024;
867 uint8 buffer[BUFFER_SIZE];
868
869 int32 length = 0;
870 src.GetLength(length);
871
872 while (length && err == 0) {
873 int32 count = length > BUFFER_SIZE ? BUFFER_SIZE : length;
874 if (src.Read(count, buffer)) {
875 if (!dst.Write(count, buffer))
876 err = dst.GetError();
877 } else
878 err = src.GetError();
879 length -= count;
880 }
881 }
882 } else
883 err = source_name.GetError();
884 if (err)
885 Delete();
886 return err == 0;
887 }
888
889 // ZZZ: Filesystem browsing list that lets user actually navigate directories...
890 class w_directory_browsing_list : public w_list<dir_entry>
891 {
892 public:
w_directory_browsing_list(const FileSpecifier & inStartingDirectory,dialog * inParentDialog)893 w_directory_browsing_list(const FileSpecifier& inStartingDirectory, dialog* inParentDialog)
894 : w_list<dir_entry>(entries, 400, 15, 0), parent_dialog(inParentDialog), current_directory(inStartingDirectory), sort_order(sort_by_name)
895 {
896 refresh_entries();
897 }
898
899
w_directory_browsing_list(const FileSpecifier & inStartingDirectory,dialog * inParentDialog,const string & inStartingFile)900 w_directory_browsing_list(const FileSpecifier& inStartingDirectory, dialog* inParentDialog, const string& inStartingFile)
901 : w_list<dir_entry>(entries, 400, 15, 0), parent_dialog(inParentDialog), current_directory(inStartingDirectory)
902 {
903 refresh_entries();
904 if(entries.size() != 0)
905 select_entry(inStartingFile, false);
906 }
907
908
set_directory_changed_callback(action_proc inCallback,void * inArg=NULL)909 void set_directory_changed_callback(action_proc inCallback, void* inArg = NULL)
910 {
911 directory_changed_proc = inCallback;
912 directory_changed_proc_arg = inArg;
913 }
914
915
draw_item(vector<dir_entry>::const_iterator i,SDL_Surface * s,int16 x,int16 y,uint16 width,bool selected) const916 void draw_item(vector<dir_entry>::const_iterator i, SDL_Surface *s, int16 x, int16 y, uint16 width, bool selected) const
917 {
918 y += font->get_ascent();
919 set_drawing_clip_rectangle(0, x, s->h, x + width);
920
921 if(i->is_directory)
922 {
923 string theName = i->name + "/";
924 draw_text(s, theName.c_str (), x, y, selected ? get_theme_color (ITEM_WIDGET, ACTIVE_STATE) : get_theme_color (ITEM_WIDGET, DEFAULT_STATE), font, style, true);
925 }
926 else
927 {
928 char date[256];
929 tm *time_info = localtime(&i->date);
930
931 if (time_info)
932 {
933 strftime(date, 256, "%x %H:%M", time_info);
934 int date_width = text_width(date, font, style);
935 draw_text(s, date, x + width - date_width, y, selected ? get_theme_color(ITEM_WIDGET, ACTIVE_STATE) : get_theme_color(ITEM_WIDGET, DEFAULT_STATE), font, style);
936 set_drawing_clip_rectangle(0, x, s->h, x + width - date_width - 4);
937 }
938 draw_text(s, FileSpecifier::HideExtension(i->name).c_str (), x, y, selected ? get_theme_color (ITEM_WIDGET, ACTIVE_STATE) : get_theme_color (ITEM_WIDGET, DEFAULT_STATE), font, style, true);
939 }
940
941 set_drawing_clip_rectangle(SHRT_MIN, SHRT_MIN, SHRT_MAX, SHRT_MAX);
942 }
943
944
can_move_up_a_level()945 bool can_move_up_a_level()
946 {
947 string base;
948 string part;
949 current_directory.SplitPath(base, part);
950 return (part != string());
951 }
952
953
move_up_a_level()954 void move_up_a_level()
955 {
956 string base;
957 string part;
958 current_directory.SplitPath(base, part);
959 if(part != string())
960 {
961 FileSpecifier parent_directory(base);
962 if(parent_directory.Exists())
963 {
964 current_directory = parent_directory;
965 refresh_entries();
966 select_entry(part, true);
967 announce_directory_changed();
968 }
969 }
970 }
971
972
item_selected(void)973 void item_selected(void)
974 {
975 current_directory.AddPart(entries[selection].name);
976
977 if(entries[selection].is_directory)
978 {
979 refresh_entries();
980 announce_directory_changed();
981 }
982 else if (file_selected)
983 {
984 file_selected(entries[selection].name);
985 }
986 }
987
988 enum SortOrder {
989 sort_by_name,
990 sort_by_date,
991 };
992
sort_by(SortOrder order)993 void sort_by(SortOrder order)
994 {
995 sort_order = order;
996 refresh_entries();
997 }
998
999
get_file()1000 const FileSpecifier& get_file() { return current_directory; }
1001
1002 boost::function<void(const std::string&)> file_selected;
1003
1004 private:
1005 vector<dir_entry> entries;
1006 dialog* parent_dialog;
1007 FileSpecifier current_directory;
1008 action_proc directory_changed_proc;
1009 void* directory_changed_proc_arg = nullptr;
1010 SortOrder sort_order = sort_by_name;
1011
1012 struct most_recent
1013 {
operator ()w_directory_browsing_list::most_recent1014 bool operator()(const dir_entry& a, const dir_entry& b)
1015 {
1016 return a.date > b.date;
1017 }
1018 };
1019
refresh_entries()1020 void refresh_entries()
1021 {
1022 if(current_directory.ReadDirectory(entries))
1023 {
1024 if (sort_order == sort_by_name)
1025 {
1026 sort(entries.begin(), entries.end());
1027 }
1028 else
1029 {
1030 sort(entries.begin(), entries.end(), most_recent());
1031 }
1032 }
1033 num_items = entries.size();
1034 new_items();
1035 }
1036
select_entry(const string & inName,bool inIsDirectory)1037 void select_entry(const string& inName, bool inIsDirectory)
1038 {
1039 dir_entry theEntryToFind(inName, NONE /* length - ignored for our purpose */, inIsDirectory);
1040 vector<dir_entry>::iterator theEntry = find(entries.begin(), entries.end(), theEntryToFind);
1041 if(theEntry != entries.end())
1042 set_selection(theEntry - entries.begin());
1043 }
1044
announce_directory_changed()1045 void announce_directory_changed()
1046 {
1047 if(directory_changed_proc != NULL)
1048 directory_changed_proc(directory_changed_proc_arg);
1049 }
1050 };
1051
1052 const char* sort_by_labels[] = {
1053 "name",
1054 "date",
1055 0
1056 };
1057
1058 // common functionality for read and write dialogs
1059 class FileDialog {
1060 public:
FileDialog()1061 FileDialog() {
1062 }
1063 virtual ~FileDialog() = default;
1064
Run()1065 bool Run() {
1066 Layout();
1067
1068 bool result = false;
1069 if (m_dialog.run() == 0)
1070 {
1071 result = true;
1072 }
1073
1074 if (get_game_state() == _game_in_progress) update_game_window();
1075 return result;
1076 }
1077
1078 protected:
Init(const FileSpecifier & dir,w_directory_browsing_list::SortOrder default_order,std::string filename)1079 void Init(const FileSpecifier& dir, w_directory_browsing_list::SortOrder default_order, std::string filename) {
1080 m_sort_by_w = new w_select(static_cast<size_t>(default_order), sort_by_labels);
1081 m_sort_by_w->set_selection_changed_callback(boost::bind(&FileDialog::on_change_sort_order, this));
1082 m_up_button_w = new w_button("UP", boost::bind(&FileDialog::on_up, this));
1083 if (filename.empty())
1084 {
1085 m_list_w = new w_directory_browsing_list(dir, &m_dialog);
1086 }
1087 else
1088 {
1089 m_list_w = new w_directory_browsing_list(dir, &m_dialog, filename);
1090 }
1091 m_list_w->sort_by(default_order);
1092 m_list_w->set_directory_changed_callback(boost::bind(&FileDialog::on_directory_changed, this));
1093
1094 dir.GetName(temporary);
1095 m_directory_name_w = new w_static_text(temporary);
1096 }
1097
1098 dialog m_dialog;
1099 w_select* m_sort_by_w;
1100 w_button* m_up_button_w;
1101 w_static_text* m_directory_name_w;
1102 w_directory_browsing_list* m_list_w;
1103
1104 private:
1105 virtual void Layout() = 0;
1106
1107
on_directory_changed()1108 void on_directory_changed() {
1109 m_list_w->get_file().GetName(temporary);
1110 m_directory_name_w->set_text(temporary);
1111
1112 m_up_button_w->set_enabled(m_list_w->can_move_up_a_level());
1113
1114 m_dialog.draw();
1115 }
1116
on_change_sort_order()1117 void on_change_sort_order() {
1118 m_list_w->sort_by(static_cast<w_directory_browsing_list::SortOrder>(m_sort_by_w->get_selection()));
1119 }
1120
on_up()1121 void on_up() {
1122 m_list_w->move_up_a_level();
1123 }
1124
1125 };
1126
1127 class ReadFileDialog : public FileDialog
1128 {
1129 public:
ReadFileDialog(FileSpecifier dir,Typecode type,const char * prompt)1130 ReadFileDialog(FileSpecifier dir, Typecode type, const char* prompt) : FileDialog(), m_prompt(prompt) {
1131 w_directory_browsing_list::SortOrder default_order = w_directory_browsing_list::sort_by_name;
1132
1133 if (!m_prompt)
1134 {
1135 switch(type)
1136 {
1137 case _typecode_savegame:
1138 m_prompt = "CONTINUE SAVED GAME";
1139 break;
1140 case _typecode_film:
1141 m_prompt = "REPLAY SAVED FILM";
1142 break;
1143 default:
1144 m_prompt = "OPEN FILE";
1145 break;
1146 }
1147 }
1148
1149 std::string filename;
1150 switch (type)
1151 {
1152 case _typecode_savegame:
1153 dir.SetToSavedGamesDir();
1154 default_order = w_directory_browsing_list::sort_by_date;
1155 break;
1156 case _typecode_film:
1157 dir.SetToRecordingsDir();
1158 break;
1159 case _typecode_scenario:
1160 case _typecode_netscript:
1161 {
1162 // Go to most recently-used directory
1163 DirectorySpecifier theDirectory;
1164 dir.SplitPath(theDirectory, filename);
1165 dir.FromDirectory(theDirectory);
1166 if (!dir.Exists())
1167 dir.SetToLocalDataDir();
1168 }
1169 break;
1170 default:
1171 dir.SetToLocalDataDir();
1172 break;
1173 }
1174
1175 Init(dir, default_order, filename);
1176
1177 m_list_w->file_selected = boost::bind(&ReadFileDialog::on_file_selected, this);
1178 }
1179 virtual ~ReadFileDialog() = default;
Layout()1180 void Layout() {
1181 vertical_placer* placer = new vertical_placer;
1182 placer->dual_add(new w_title(m_prompt), m_dialog);
1183 placer->add(new w_spacer, true);
1184
1185 #ifndef MAC_APP_STORE
1186 placer->dual_add(m_directory_name_w, m_dialog);
1187
1188 placer->add(new w_spacer(), true);
1189 #endif
1190
1191 horizontal_placer* top_row_placer = new horizontal_placer;
1192
1193 top_row_placer->dual_add(m_sort_by_w->label("Sorted by: "), m_dialog);
1194 top_row_placer->dual_add(m_sort_by_w, m_dialog);
1195 top_row_placer->add_flags(placeable::kFill);
1196 top_row_placer->add(new w_spacer, true);
1197 top_row_placer->add_flags();
1198 #ifndef MAC_APP_STORE
1199 top_row_placer->dual_add(m_up_button_w, m_dialog);
1200 #endif
1201
1202 placer->add_flags(placeable::kFill);
1203 placer->add(top_row_placer, true);
1204 placer->add_flags();
1205
1206 placer->dual_add(m_list_w, m_dialog);
1207 placer->add(new w_spacer, true);
1208
1209 horizontal_placer* button_placer = new horizontal_placer;
1210 button_placer->dual_add(new w_button("CANCEL", dialog_cancel, &m_dialog), m_dialog);
1211
1212 placer->add(button_placer, true);
1213
1214 m_dialog.activate_widget(m_list_w);
1215 m_dialog.set_widget_placer(placer);
1216 }
1217
GetFile()1218 FileSpecifier GetFile() {
1219 return m_list_w->get_file();
1220 }
1221
1222 private:
on_file_selected()1223 void on_file_selected() {
1224 m_dialog.quit(0);
1225 }
1226
1227 const char* m_prompt;
1228 std::string m_filename;
1229 };
1230
ReadDialog(Typecode type,const char * prompt)1231 bool FileSpecifier::ReadDialog(Typecode type, const char *prompt)
1232 {
1233 ReadFileDialog d(*this, type, prompt);
1234 if (d.Run())
1235 {
1236 *this = d.GetFile();
1237 return true;
1238 }
1239 else
1240 {
1241 return false;
1242 }
1243 }
1244
1245 class w_file_name : public w_text_entry {
1246 public:
w_file_name(dialog * d,const char * initial_name=NULL)1247 w_file_name(dialog *d, const char *initial_name = NULL) : w_text_entry(31, initial_name), parent(d) {}
~w_file_name()1248 ~w_file_name() {}
1249
event(SDL_Event & e)1250 void event(SDL_Event & e)
1251 {
1252 // Return = close dialog
1253 if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_RETURN)
1254 parent->quit(0);
1255 w_text_entry::event(e);
1256 }
1257
1258 private:
1259 dialog *parent;
1260 };
1261
1262 class WriteFileDialog : public FileDialog
1263 {
1264 public:
WriteFileDialog(FileSpecifier dir,Typecode type,const char * prompt,const char * default_name)1265 WriteFileDialog(FileSpecifier dir, Typecode type, const char* prompt, const char* default_name) : FileDialog(), m_prompt(prompt), m_default_name(default_name), m_extension(0) {
1266 if (!m_prompt)
1267 {
1268 switch (type)
1269 {
1270 case _typecode_savegame:
1271 m_prompt = "SAVE GAME";
1272 break;
1273 case _typecode_film:
1274 m_prompt = "SAVE FILM";
1275 break;
1276 case _typecode_movie:
1277 m_prompt = "EXPORT FILM";
1278 break;
1279 default:
1280 m_prompt = "SAVE FILE";
1281 break;
1282 }
1283 }
1284
1285 switch (type)
1286 {
1287 case _typecode_savegame:
1288 m_extension = ".sgaA";
1289 break;
1290 case _typecode_film:
1291 m_extension = ".filA";
1292 break;
1293 default:
1294 break;
1295 }
1296
1297 if (m_extension && boost::algorithm::ends_with(m_default_name, m_extension))
1298 {
1299 m_default_name.resize(m_default_name.size() - strlen(m_extension));
1300 }
1301
1302 w_directory_browsing_list::SortOrder default_order = w_directory_browsing_list::sort_by_name;
1303 switch (type) {
1304 case _typecode_savegame:
1305 {
1306 string base;
1307 string part;
1308 dir.SplitPath(base, part);
1309 if (part != string())
1310 {
1311 dir = base;
1312 }
1313 default_order = w_directory_browsing_list::sort_by_date;
1314 }
1315 break;
1316 case _typecode_film:
1317 case _typecode_movie:
1318 dir.SetToRecordingsDir();
1319 break;
1320 default:
1321 dir.SetToLocalDataDir();
1322 break;
1323 }
1324
1325 Init(dir, default_order, m_default_name);
1326
1327 m_list_w->file_selected = boost::bind(&WriteFileDialog::on_file_selected, this, _1);
1328 }
1329 virtual ~WriteFileDialog() = default;
Layout()1330 void Layout() {
1331 vertical_placer* placer = new vertical_placer;
1332 placer->dual_add(new w_title(m_prompt), m_dialog);
1333 placer->add(new w_spacer, true);
1334
1335 #ifndef MAC_APP_STORE
1336 placer->dual_add(m_directory_name_w, m_dialog);
1337
1338 placer->add(new w_spacer(), true);
1339 #endif
1340
1341 horizontal_placer* top_row_placer = new horizontal_placer;
1342
1343 top_row_placer->dual_add(m_sort_by_w->label("Sorted by: "), m_dialog);
1344 top_row_placer->dual_add(m_sort_by_w, m_dialog);
1345 top_row_placer->add_flags(placeable::kFill);
1346 top_row_placer->add(new w_spacer, true);
1347 top_row_placer->add_flags();
1348 #ifndef MAC_APP_STORE
1349 top_row_placer->dual_add(m_up_button_w, m_dialog);
1350 #endif
1351
1352 placer->add_flags(placeable::kFill);
1353 placer->add(top_row_placer, true);
1354 placer->add_flags();
1355
1356 placer->dual_add(m_list_w, m_dialog);
1357 placer->add(new w_spacer, true);
1358
1359 placer->add_flags(placeable::kFill);
1360
1361 horizontal_placer* file_name_placer = new horizontal_placer;
1362 m_name_w = new w_file_name(&m_dialog, m_default_name.c_str());
1363 #ifdef MAC_APP_STORE
1364 file_name_placer->dual_add(m_name_w->label("Name:"), m_dialog);
1365 #else
1366 file_name_placer->dual_add(m_name_w->label("File Name:"), m_dialog);
1367 #endif
1368 file_name_placer->add_flags(placeable::kFill);
1369 file_name_placer->dual_add(m_name_w, m_dialog);
1370
1371 placer->add_flags(placeable::kFill);
1372 placer->add(file_name_placer, true);
1373 placer->add_flags();
1374 placer->add(new w_spacer, true);
1375
1376 horizontal_placer* button_placer = new horizontal_placer;
1377 button_placer->dual_add(new w_button("OK", dialog_ok, &m_dialog), m_dialog);
1378 button_placer->dual_add(new w_button("CANCEL", dialog_cancel, &m_dialog), m_dialog);
1379
1380 placer->add(button_placer, true);
1381
1382 m_dialog.activate_widget(m_name_w);
1383 m_dialog.set_widget_placer(placer);
1384 }
1385
GetPath()1386 FileSpecifier GetPath() {
1387 FileSpecifier dir = m_list_w->get_file();
1388 std::string base;
1389 std::string part;
1390 dir.SplitPath(base, part);
1391
1392 std::string filename = GetFilename();
1393 if (part == filename)
1394 {
1395 dir = base;
1396 }
1397
1398 if (m_extension && !boost::algorithm::ends_with(filename, m_extension))
1399 {
1400 filename += m_extension;
1401 }
1402 dir.AddPart(filename);
1403 return dir;
1404 }
1405
GetFilename()1406 std::string GetFilename() {
1407 return m_name_w->get_text();
1408 }
1409
1410 private:
on_file_selected(const std::string & filename)1411 void on_file_selected(const std::string& filename) {
1412 m_name_w->set_text(filename.c_str());
1413 m_dialog.quit(0);
1414 }
1415
1416 const char* m_prompt;
1417 std::string m_default_name;
1418 const char* m_extension;
1419 w_file_name* m_name_w;
1420 };
1421
1422 static bool confirm_save_choice(FileSpecifier & file);
1423
WriteDialog(Typecode type,const char * prompt,const char * default_name)1424 bool FileSpecifier::WriteDialog(Typecode type, const char *prompt, const char *default_name)
1425 {
1426 again:
1427 WriteFileDialog d(*this, type, prompt, default_name);
1428 bool result = false;
1429 if (d.Run())
1430 {
1431 if (d.GetFilename().empty())
1432 {
1433 play_dialog_sound(DIALOG_ERROR_SOUND);
1434 goto again;
1435 }
1436
1437 *this = d.GetPath();
1438
1439 if (!confirm_save_choice(*this))
1440 {
1441 goto again;
1442 }
1443
1444 result = true;
1445 }
1446
1447 return result;
1448
1449 }
1450
WriteDialogAsync(Typecode type,char * prompt,char * default_name)1451 bool FileSpecifier::WriteDialogAsync(Typecode type, char *prompt, char *default_name)
1452 {
1453 return FileSpecifier::WriteDialog(type, prompt, default_name);
1454 }
1455
confirm_save_choice(FileSpecifier & file)1456 static bool confirm_save_choice(FileSpecifier & file)
1457 {
1458 // If the file doesn't exist, everything is alright
1459 if (!file.Exists())
1460 return true;
1461
1462 // Construct message
1463 char name[256];
1464 file.GetName(name);
1465 char message[512];
1466 sprintf(message, "'%s' already exists.", FileSpecifier::HideExtension(std::string(name)).c_str());
1467
1468 // Create dialog
1469 dialog d;
1470 vertical_placer *placer = new vertical_placer;
1471 placer->dual_add(new w_static_text(message), d);
1472 placer->dual_add(new w_static_text("Ok to overwrite?"), d);
1473 placer->add(new w_spacer(), true);
1474
1475 horizontal_placer *button_placer = new horizontal_placer;
1476 w_button *default_button = new w_button("YES", dialog_ok, &d);
1477 button_placer->dual_add(default_button, d);
1478 button_placer->dual_add(new w_button("NO", dialog_cancel, &d), d);
1479
1480 placer->add(button_placer, true);
1481
1482 d.activate_widget(default_button);
1483
1484 d.set_widget_placer(placer);
1485
1486 // Run dialog
1487 return d.run() == 0;
1488 }
1489
ScopedSearchPath(const DirectorySpecifier & dir)1490 ScopedSearchPath::ScopedSearchPath(const DirectorySpecifier& dir)
1491 {
1492 data_search_path.insert(data_search_path.begin(), dir);
1493 }
1494
~ScopedSearchPath()1495 ScopedSearchPath::~ScopedSearchPath()
1496 {
1497 data_search_path.erase(data_search_path.begin());
1498 }
1499