1 /*
2  *
3  *  Iter Vehemens ad Necem (IVAN)
4  *  Copyright (C) Timo Kiviluoto
5  *  Released under the GNU General
6  *  Public License
7  *
8  *  See LICENSING which should be included
9  *  along with this file for more details
10  *
11  */
12 
13 #include <cctype>
14 
15 #ifdef WIN32
16 #include <windows.h>
17 #include <direct.h> // for _mkdir
18 #else
19 #include <dirent.h>
20 #include <sys/stat.h> // for mkdir
21 #endif
22 
23 #include "save.h"
24 #include "femath.h"
25 
26 #include "dbgmsgproj.h"
27 
28 truth outputfile::bakcupBeforeSaving = true;
29 truth outputfile::saveOnNewFileAlways = true;
30 
Close()31 void outputfile::Close() { DBG3(FileNameNewTmp.CStr(),FileName.CStr(),saveOnNewFileAlways);
32   File.close();
33 
34   if(saveOnNewFileAlways){ // This moves the new .tmp file to the correct filename, before removing it.
35     if(FileNameNewTmp.IsEmpty())
36       ABORT("new tmp filename is empty, 'save on new file always' option was properly set before this output file '%s' ?",FileName.CStr());
37 
38     std::ifstream newTmpFile(FileNameNewTmp.CStr(), std::ios::binary);
39     if(newTmpFile.good()){
40       std::remove(FileName.CStr()); //just to not let existing one create any doubts if some crash happens here
41 
42       std::ofstream final(FileName.CStr(), std::ios::binary);
43       final << newTmpFile.rdbuf();
44 
45       final.close();
46       newTmpFile.close();
47 
48       std::remove(FileNameNewTmp.CStr()); //last thing
49     }else{
50       ABORT("the new tmp file '%s' should exist!",FileNameNewTmp.CStr());
51     }
52   }
53 }
54 
outputfile(cfestring & fileName,truth AbortOnErr)55 outputfile::outputfile(cfestring& fileName, truth AbortOnErr)
56 : FileName(fileName)
57 {
58   if(FileName.IsEmpty())
59     ABORT("empty output filename");
60 
61   // If FileName contains a directory, make sure the directory exists first.
62   festring::csizetype LastPathSeparatorPos = FileName.FindLast('/');
63   if(LastPathSeparatorPos != festring::NPos)
64   {
65     festring DirectoryPath = FileName;
66     DirectoryPath.Resize(LastPathSeparatorPos);
67     MakePath(DirectoryPath);
68   }
69 
70   if(bakcupBeforeSaving){ // this is not that useful, the tmp files are better as they prevent overwriting the final files in case the game crashes
71     std::ifstream orig(FileName.CStr(), std::ios::binary);
72     if(orig.good()){
73       festring fsBkp;fsBkp << FileName << ".bkp";
74       std::remove(fsBkp.CStr()); //just to have a granted clean new backup
75 
76       std::ofstream bkp(fsBkp.CStr(), std::ios::binary);
77       bkp << orig.rdbuf();
78 
79       orig.close();
80       bkp.close();
81 
82       // backups are kept until leaving the game
83     }
84   }
85 
86   festring FileNameNew(FileName);
87   if(saveOnNewFileAlways){
88     FileNameNew<<".tmp";
89     FileNameNewTmp<<FileNameNew;
90 
91     /**
92      * if happened a crash and a tmp file is still there,
93      * it is most probably invalid and incomplete,
94      * so just remove it.
95      */
96     std::remove(FileNameNew.CStr());
97   }
98 
99   File.open(FileNameNew.CStr(), std::ios::binary);
100 
101   if(AbortOnErr && !IsOpen())
102     ABORT("Can't open %s for output!", FileNameNew.CStr());
103 }
104 
inputfile(cfestring & FileName,const valuemap * ValueMap,truth AbortOnErr)105 inputfile::inputfile(cfestring& FileName,
106                      const valuemap* ValueMap,
107                      truth AbortOnErr)
108 : File(FileName.CStr(), std::ios::binary),
109   FileName(FileName),
110   ValueMap(ValueMap),
111   lastWordWasString(false)
112 {
113   if(AbortOnErr && !IsOpen())
114     ABORT("File %s not found!", FileName.CStr());
115 }
116 
ReadWord(truth AbortOnEOF)117 festring inputfile::ReadWord(truth AbortOnEOF)
118 {
119   /*static*/ festring ToReturn;
120   ReadWord(ToReturn, AbortOnEOF);
121   return ToReturn;
122 }
123 
124 #define MODE_WORD 1
125 #define MODE_NUMBER 2
126 
127 #define PUNCT_RETURN 0
128 #define PUNCT_CONTINUE 1
129 
HandlePunct(festring & String,int Char,int Mode)130 int inputfile::HandlePunct(festring& String, int Char, int Mode)
131 {
132   if(Char == '/')
133   {
134     if(!Eof())
135     {
136       Char = Get();
137 
138       if(Char == '*')
139       {
140         long StartPos = TellPos();
141         int OldChar = 0, CommentLevel = 1;
142 
143         for(;;)
144         {
145           if(Eof())
146             ABORT("Unterminated comment in file %s, beginning at line %ld!",
147                   FileName.CStr(), TellLineOfPos(StartPos));
148 
149           Char = Get();
150 
151           if(OldChar != '*' || Char != '/')
152           {
153             if(OldChar != '/' || Char != '*')
154               OldChar = Char;
155             else
156             {
157               ++CommentLevel;
158               OldChar = 0;
159             }
160           }
161           else
162           {
163             if(!--CommentLevel)
164               break;
165             else
166               OldChar = 0;
167           }
168         }
169 
170         return PUNCT_CONTINUE;
171       }
172       else
173       {
174         File.putback(Char);
175         File.clear();
176       }
177     }
178 
179     if(Mode)
180       File.putback('/');
181     else
182       String << '/';
183 
184     return PUNCT_RETURN;
185   }
186 
187   if(Mode)
188   {
189     File.putback(Char);
190     return PUNCT_RETURN;
191   }
192 
193   if(Char == '"')
194   {
195     lastWordWasString = true;
196     long StartPos = TellPos();
197     int OldChar = 0;
198 
199     for(;;)
200     {
201       if(Eof())
202         ABORT("Unterminated string in file %s, beginning at line %ld!",
203               FileName.CStr(), TellLineOfPos(StartPos));
204 
205       Char = Get();
206 
207       if(Char != '"')
208       {
209         String << char(Char);
210         OldChar = Char;
211       }
212       else if(OldChar == '\\')
213       {
214         String[String.GetSize() - 1] = '"';
215         OldChar = 0;
216       }
217       else
218         return PUNCT_RETURN;
219     }
220   }
221 
222   String << char(Char);
223   return PUNCT_RETURN;
224 }
225 
ReadWord(festring & String,truth AbortOnEOF)226 void inputfile::ReadWord(festring& String, truth AbortOnEOF)
227 {
228   int Mode = 0;
229   String.Empty();
230   lastWordWasString = false;
231 
232   for(int Char = Get(); !Eof(); Char = Get())
233   {
234     if(isalpha(Char) || Char == '_')
235     {
236       if(!Mode)
237         Mode = MODE_WORD;
238       else if(Mode == MODE_NUMBER)
239       {
240         File.putback(Char);
241         return;
242       }
243 
244       String << char(Char);
245       continue;
246     }
247 
248     if(isdigit(Char))
249     {
250       if(!Mode)
251         Mode = MODE_NUMBER;
252       else if(Mode == MODE_WORD)
253       {
254         File.putback(Char);
255         return;
256       }
257 
258       String << char(Char);
259       continue;
260     }
261 
262     if((Char == ' ' || Char == '\n') && Mode)
263       return;
264 
265     if(ispunct(Char) && HandlePunct(String, Char, Mode) == PUNCT_RETURN)
266       return;
267   }
268 
269   if(AbortOnEOF)
270     ABORT("Unexpected end of file %s!", FileName.CStr());
271 
272   if(Mode)
273     File.clear();
274 }
275 
ReadLetter(truth AbortOnEOF)276 char inputfile::ReadLetter(truth AbortOnEOF)
277 {
278   for(int Char = Get(); !Eof(); Char = Get())
279   {
280     if(isalpha(Char) || isdigit(Char))
281       return Char;
282 
283     if(ispunct(Char))
284     {
285       if(Char == '/')
286       {
287         if(!Eof())
288         {
289           Char = Get();
290 
291           if(Char == '*')
292           {
293             long StartPos = TellPos();
294             int OldChar = 0, CommentLevel = 1;
295 
296             for(;;)
297             {
298               if(Eof())
299                 ABORT("Unterminated comment in file %s, "
300                       "beginning at line %ld!",
301                       FileName.CStr(), TellLineOfPos(StartPos));
302 
303               Char = Get();
304 
305               if(OldChar != '*' || Char != '/')
306               {
307                 if(OldChar != '/' || Char != '*')
308                   OldChar = Char;
309                 else
310                 {
311                   ++CommentLevel;
312                   OldChar = 0;
313                 }
314               }
315               else
316               {
317                 if(!--CommentLevel)
318                   break;
319                 else
320                   OldChar = 0;
321               }
322             }
323 
324             continue;
325           }
326           else
327             File.putback(Char);
328         }
329 
330         return '/';
331       }
332 
333       return Char;
334     }
335   }
336 
337   if(AbortOnEOF)
338     ABORT("Unexpected end of file %s!", FileName.CStr());
339 
340   return 0;
341 }
342 
343 /* Reads a number or a formula from inputfile. Valid values could be for
344    instance "3", "5 * 4+5", "2+Variable%4" etc. */
345 
ReadNumberIntr(int CallLevel,long * num,truth * isString,truth allowStr,truth PreserveTerminator)346 festring inputfile::ReadNumberIntr(int CallLevel, long *num, truth *isString, truth allowStr, truth PreserveTerminator)
347 {
348   long Value = 0;
349   /*static*/ festring Word;
350   truth NumberCorrect = false;
351   truth firstWord = true;
352   *isString = false;
353   *num = 0;
354   festring res;
355 
356   for(;;)
357   {
358     ReadWord(Word);
359     if(firstWord)
360     {
361       if(allowStr && lastWordWasString)
362       {
363         *isString = true;
364         return Word;
365       }
366       firstWord = false;
367     }
368     char First = Word[0];
369 
370     if(isdigit(First))
371     {
372       Value = atoi(Word.CStr());
373       NumberCorrect = true;
374       continue;
375     }
376 
377     if(Word.GetSize() == 1)
378     {
379       if(First == ';' || First == ',' || First == ':')
380       {
381         if(CallLevel != HIGHEST || PreserveTerminator)
382           File.putback(First);
383 
384         *num = Value;
385         return res;
386       }
387 
388       if(First == ')')
389       {
390         if((CallLevel != HIGHEST && CallLevel != 4) || PreserveTerminator)
391           File.putback(')');
392 
393         *num = Value;
394         return res;
395       }
396 
397       if(First == '~')
398       {
399         Value = ~ReadNumber(4);
400         NumberCorrect = true;
401         continue;
402       }
403 
404       /* Convert this into an inline function! */
405 
406 #define CHECK_OP(op, cl)\
407           if(First == #op[0])\
408           {\
409             if(cl < CallLevel)\
410               {\
411                 Value op##= ReadNumber(cl);\
412                 NumberCorrect = true;\
413                 continue;\
414               }\
415             else\
416               {\
417                 File.putback(#op[0]);\
418                 *num = Value;\
419                 return res;\
420               }\
421           }
422 
423       CHECK_OP(&, 1); CHECK_OP(|, 1); CHECK_OP(^, 1);
424       CHECK_OP(*, 2); CHECK_OP(/, 2); CHECK_OP(%, 2);
425       CHECK_OP(+, 3); CHECK_OP(-, 3);
426 
427       if(First == '<')
428       {
429         char Next = Get();
430 
431         if(Next == '<')
432           if(1 < CallLevel)
433           {
434             Value <<= ReadNumber(1);
435             NumberCorrect = true;
436             continue;
437           }
438           else
439           {
440             File.putback('<');
441             File.putback('<');
442             *num = Value;
443             return res;
444           }
445         else
446           File.putback(Next);
447       }
448 
449       if(First == '>')
450       {
451         char Next = Get();
452 
453         if(Next == '>')
454           if(1 < CallLevel)
455           {
456             Value >>= ReadNumber(1);
457             NumberCorrect = true;
458             continue;
459           }
460           else
461           {
462             File.putback('>');
463             File.putback('>');
464             *num = Value;
465             return res;
466           }
467         else
468           File.putback(Next);
469       }
470 
471       if(First == '(')
472       {
473         if(NumberCorrect)
474         {
475           File.putback('(');
476           *num = Value;
477           return res;
478         }
479         else
480         {
481           Value = ReadNumber(4);
482           NumberCorrect = false;
483           continue;
484         }
485       }
486 
487       if(First == '=' && CallLevel == HIGHEST)
488         continue;
489 
490       if(First == '#') // for #defines
491       {
492         File.putback('#');
493         *num = Value;
494         return res;
495       }
496     }
497 
498     if(Word == "rgb")
499     {
500       int Bits = ReadNumber();
501 
502       if(Bits == 16)
503       {
504         int Red = ReadNumber();
505         int Green = ReadNumber();
506         int Blue = ReadNumber();
507         Value = MakeRGB16(Red, Green, Blue);
508       }
509       else if(Bits == 24)
510       {
511         int Red = ReadNumber();
512         int Green = ReadNumber();
513         int Blue = ReadNumber();
514         Value = MakeRGB24(Red, Green, Blue);
515       }
516       else
517         ABORT("Illegal RGB bit size %d in file %s, line %ld!",
518               Bits, FileName.CStr(), TellLine());
519 
520       NumberCorrect = true;
521       continue;
522     }
523 
524     if(Word == "true")
525     {
526       Value = 1;
527       NumberCorrect = true;
528       continue;
529     }
530 
531     if(Word == "false")
532     {
533       Value = 0;
534       NumberCorrect = true;
535       continue;
536     }
537 
538     if(ValueMap)
539     {
540       valuemap::const_iterator Iterator = ValueMap->find(Word);
541 
542       if(Iterator != ValueMap->end())
543       {
544         Value = Iterator->second;
545         NumberCorrect = true;
546         continue;
547       }
548     }
549 
550     ABORT("Odd numeric value \"%s\" encountered in file %s, line %ld!",
551           Word.CStr(), FileName.CStr(), TellLine());
552   }
553 }
554 
ReadNumber(int CallLevel,truth PreserveTerminator)555 long inputfile::ReadNumber(int CallLevel, truth PreserveTerminator)
556 {
557   long num = 0;
558   truth isString = false;
559   ReadNumberIntr(CallLevel, &num, &isString, false, PreserveTerminator);
560   return num;
561 }
562 
ReadStringOrNumber(long * num,truth * isString,truth PreserveTerminator)563 festring inputfile::ReadStringOrNumber(long *num, truth *isString, truth PreserveTerminator)
564 {
565   return ReadNumberIntr(0xFF, num, isString, true, PreserveTerminator);
566 }
567 
ReadVector2d()568 v2 inputfile::ReadVector2d()
569 {
570   v2 Vector;
571   Vector.X = ReadNumber();
572   Vector.Y = ReadNumber();
573   return Vector;
574 }
575 
ReadRect()576 rect inputfile::ReadRect()
577 {
578   rect Rect;
579   Rect.X1 = ReadNumber();
580   Rect.Y1 = ReadNumber();
581   Rect.X2 = ReadNumber();
582   Rect.Y2 = ReadNumber();
583   return Rect;
584 }
585 
operator <<(outputfile & SaveFile,cfestring & String)586 outputfile& operator<<(outputfile& SaveFile, cfestring& String)
587 {
588   ushort Length = String.GetSize();
589   SaveFile << Length;
590 
591   if(Length)
592     SaveFile.Write(String.CStr(), Length);
593 
594   return SaveFile;
595 }
596 
operator >>(inputfile & SaveFile,festring & String)597 inputfile& operator>>(inputfile& SaveFile, festring& String)
598 {
599   char* RealBuffer, StackBuffer[1024];
600   ushort Length;
601   SaveFile >> Length;
602   RealBuffer = Length < 1024 ? StackBuffer : new char[Length + 1];
603   String.Empty();
604 
605   if(Length)
606   {
607     SaveFile.Read(RealBuffer, Length);
608     RealBuffer[Length] = 0;
609     String << RealBuffer;
610   }
611 
612   if(Length >= 1024)
613     delete [] RealBuffer;
614 
615   return SaveFile;
616 }
617 
operator <<(outputfile & SaveFile,cchar * String)618 outputfile& operator<<(outputfile& SaveFile, cchar* String)
619 {
620   ushort Length = String ? strlen(String) : 0;
621   SaveFile << Length;
622 
623   if(Length)
624     SaveFile.Write(String, Length);
625 
626   return SaveFile;
627 }
628 
operator >>(inputfile & SaveFile,char * & String)629 inputfile& operator>>(inputfile& SaveFile, char*& String)
630 {
631   ushort Length;
632   SaveFile >> Length;
633 
634   if(Length)
635   {
636     String = new char[Length + 1];
637     SaveFile.Read(String, Length);
638     String[Length] = 0;
639   }
640   else
641     String = 0;
642 
643   return SaveFile;
644 }
645 
ReadData(festring & String,inputfile & SaveFile)646 void ReadData(festring& String, inputfile& SaveFile)
647 {
648   SaveFile.ReadWord(String);
649 
650   if(String == "=")
651     SaveFile.ReadWord(String);
652 
653   SaveFile.ReadWord();
654 }
655 
ReadData(fearray<long> & Array,inputfile & SaveFile)656 void ReadData(fearray<long>& Array, inputfile& SaveFile)
657 {
658   Array.Clear();
659   /*static*/ festring Word;
660   SaveFile.ReadWord(Word);
661 
662   if(Word == "=")
663     SaveFile.ReadWord(Word);
664 
665   if(Word == "=")
666   {
667     Array.Allocate(1);
668     Array.Data[0] = SaveFile.ReadNumber();
669     return;
670   }
671 
672   if(Word != "{")
673     ABORT("Array syntax error \"%s\" found in file %s, line %ld!",
674           Word.CStr(), SaveFile.GetFileName().CStr(), SaveFile.TellLine());
675 
676   fearray<long>::sizetype Size = SaveFile.ReadNumber();
677   Array.Allocate(Size);
678 
679   for(fearray<long>::sizetype c = 0; c < Size; ++c)
680     Array.Data[c] = SaveFile.ReadNumber();
681 
682   if(SaveFile.ReadWord() != "}")
683     ABORT("Illegal array terminator \"%s\" "
684           "encountered in file %s, line %ld!",
685           Word.CStr(), SaveFile.GetFileName().CStr(),
686           SaveFile.TellLine());
687 }
688 
ReadData(fearray<festring> & Array,inputfile & SaveFile)689 void ReadData(fearray<festring>& Array, inputfile& SaveFile)
690 {
691   Array.Clear();
692   /*static*/ festring Word;
693   SaveFile.ReadWord(Word);
694 
695   if(Word == "=")
696     SaveFile.ReadWord(Word);
697 
698   if(Word == "=")
699   {
700     Array.Allocate(1);
701     SaveFile.ReadWord(Array.Data[0]);
702 
703     if(SaveFile.ReadWord() != ";")
704       ABORT("Array syntax error \"%s\" found in file %s, line %ld!",
705             Word.CStr(), SaveFile.GetFileName().CStr(), SaveFile.TellLine());
706 
707     return;
708   }
709 
710   if(Word != "{")
711     ABORT("Array syntax error \"%s\" found in file %s, line %ld!",
712           Word.CStr(), SaveFile.GetFileName().CStr(), SaveFile.TellLine());
713 
714   fearray<festring>::sizetype Size = SaveFile.ReadNumber();
715   Array.Allocate(Size);
716 
717   for(fearray<festring>::sizetype c = 0; c < Size; ++c)
718   {
719     SaveFile.ReadWord(Array.Data[c]);
720     SaveFile.ReadWord(Word);
721 
722     if(Word != "," && Word != ";")
723       ABORT("Array syntax error \"%s\" found in file %s, line %ld!",
724             Word.CStr(), SaveFile.GetFileName().CStr(), SaveFile.TellLine());
725   }
726 
727   if(SaveFile.ReadWord() != "}")
728     ABORT("Illegal array terminator \"%s\" "
729           "encountered in file %s, line %ld!",
730           Word.CStr(), SaveFile.GetFileName().CStr(),
731           SaveFile.TellLine());
732 }
733 
TellLineOfPos(long Pos)734 ulong inputfile::TellLineOfPos(long Pos)
735 {
736   ulong Line = 1;
737   long BackupPos = TellPos();
738   SeekPosBegin(0);
739 
740   while(TellPos() != Pos)
741     if(Get() == '\n')
742       ++Line;
743 
744   if(TellPos() != BackupPos)
745     SeekPosBegin(BackupPos);
746 
747   return Line;
748 }
749 
750 /* Returns a vector of absolute file paths referring to files inside Directory
751  * with the given Extension. */
752 
ListFiles(festring Directory,cfestring & Extension)753 std::vector<festring> ListFiles(festring Directory, cfestring& Extension)
754 {
755   std::vector<festring> Files;
756   if(Directory[Directory.GetSize() - 1] != '/')
757     Directory << '/';
758 
759 #ifdef WIN32
760   festring SearchPath = Directory + "*" + Extension;
761   WIN32_FIND_DATA fd;
762   HANDLE hFind = ::FindFirstFile(SearchPath.CStr(), &fd);
763   if(hFind == INVALID_HANDLE_VALUE)
764     ABORT("Cannot open directory %s", Directory.CStr());
765 
766   do
767   {
768     if(!(fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
769       Files.push_back(Directory + fd.cFileName);
770   }
771   while(::FindNextFile(hFind, &fd));
772   ::FindClose(hFind);
773 #else
774 #ifdef __DJGPP__
775 #error "ListFiles() not implemented on DOS!"
776 #else
777   DIR* dirp = opendir(Directory.CStr());
778   if(!dirp)
779     ABORT("Cannot open directory %s", Directory.CStr());
780 
781   dirent* dp;
782   while((dp = readdir(dirp)) != nullptr)
783   {
784     cint ExtIndex = strlen(dp->d_name) - Extension.GetSize();
785     if(ExtIndex < 0) continue;
786     if(strcmp(dp->d_name + ExtIndex, Extension.CStr()) == 0)
787       Files.push_back(Directory + dp->d_name);
788   }
789   closedir(dirp);
790 #endif
791 #endif
792   return Files;
793 }
794 
795 /* Postcondition: Path exists and is a directory. */
796 
MakePath(cfestring & Path)797 void MakePath(cfestring& Path)
798 {
799   for(festring::sizetype Pos = 0; Pos != Path.GetSize();)
800   {
801     Pos = Path.Find('/', Pos + 1);
802     if (Pos == festring::NPos) Pos = Path.GetSize();
803     cfestring DirectoryPath = festring(Path.CStr(), Pos) + '\0';
804 #ifdef WIN32
805     _mkdir(DirectoryPath.CStr());
806 #else
807     mkdir(DirectoryPath.CStr(), S_IRWXU|S_IRWXG);
808 #endif
809   }
810 }
811 
GetUserDataDir()812 festring GetUserDataDir()
813 {
814 #ifdef UNIX
815   festring Dir;
816 #ifdef MAC_APP
817   Dir << getenv("HOME") << "/Library/Application Support/IVAN/";
818 #else
819   char *xdg_home = getenv("XDG_DATA_HOME"); // check if XDG_DATA_HOME is set
820   if (xdg_home != NULL) { // if it is, use that directory
821           Dir << xdg_home << "/ivan/";
822   } else { // otherwise, default to home directory
823           Dir << getenv("HOME") << "/.ivan/";
824   }
825 #endif /* MAC_APP */
826 #ifdef DBGMSG
827   dbgmsg::SetDebugLogPath(Dir.CStr());
828 #endif /* DBGMSG */
829   return Dir;
830 #endif /* UNIX */
831 
832 #if defined(WIN32) || defined(__DJGPP__)
833   return "./";
834 #endif
835 }
836