1 /* ScummVM - Graphic Adventure Engine
2 *
3 * ScummVM is the legal property of its developers, whose names
4 * are too numerous to list here. Please refer to the COPYRIGHT
5 * file distributed with this source distribution.
6 *
7 * This program is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation; either version 2
10 * of the License, or (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program; if not, write to the Free Software
19 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 *
21 */
22
23 #include "glk/quetzal.h"
24 #include "glk/glk_api.h"
25 #include "glk/events.h"
26 #include "common/system.h"
27 #include "common/language.h"
28 #include "common/memstream.h"
29 #include "common/translation.h"
30
31 namespace Glk {
32
33 /*--------------------------------------------------------------------------*/
34
getInterpreterTag(InterpreterType interpType)35 uint32 QuetzalBase::getInterpreterTag(InterpreterType interpType) {
36 switch (interpType) {
37 case INTERPRETER_ADRIFT:
38 return MKTAG('A', 'D', 'R', 'I');
39 case INTERPRETER_ADVSYS:
40 return MKTAG('A', 'S', 'Y', 'S');
41 case INTERPRETER_AGT:
42 return MKTAG('A', 'G', 'T', ' ');
43 case INTERPRETER_AGILITY:
44 return MKTAG('A', 'G', 'I', 'L');
45 case INTERPRETER_ALAN2:
46 return MKTAG('A', 'L', 'N', '2');
47 case INTERPRETER_ALAN3:
48 return MKTAG('A', 'L', 'N', '3');
49 case INTERPRETER_ARCHETYPE:
50 return MKTAG('A', 'R', 'C', 'H');
51 case INTERPRETER_COMPREHEND:
52 return MKTAG('C', 'O', 'M', 'P');
53 case INTERPRETER_GEAS:
54 return MKTAG('G', 'E', 'A', 'S');
55 case INTERPRETER_GLULX:
56 return MKTAG('G', 'L', 'U', 'L');
57 case INTERPRETER_HUGO:
58 return MKTAG('H', 'U', 'G', 'O');
59 case INTERPRETER_JACL:
60 return MKTAG('J', 'A', 'C', 'L');
61 case INTERPRETER_LEVEL9:
62 return MKTAG('L', 'V', 'L', '9');
63 case INTERPRETER_MAGNETIC:
64 return MKTAG('M', 'A', 'G', 'N');
65 case INTERPRETER_QUEST:
66 return MKTAG('Q', 'U', 'E', 'S');
67 case INTERPRETER_SCARE:
68 return MKTAG('S', 'C', 'A', 'R');
69 case INTERPRETER_SCOTT:
70 return MKTAG('S', 'C', 'O', 'T');
71 case INTERPRETER_TADS2:
72 return MKTAG('T', 'A', 'D', '2');
73 case INTERPRETER_TADS3:
74 return MKTAG('T', 'A', 'D', '3');
75 case INTERPRETER_ZCODE:
76 return MKTAG('Z', 'C', 'O', 'D');
77 default:
78 error("Invalid interpreter type");
79 }
80 }
81
82 /*--------------------------------------------------------------------------*/
83
clear()84 void QuetzalReader::clear() {
85 _chunks.clear();
86 _stream = nullptr;
87 }
88
open(Common::SeekableReadStream * stream,uint32 formType)89 bool QuetzalReader::open(Common::SeekableReadStream *stream, uint32 formType) {
90 clear();
91 stream->seek(0);
92 _stream = stream;
93
94 if (stream->readUint32BE() != ID_FORM)
95 return false;
96
97 uint32 size = stream->readUint32BE();
98 uint32 fileFormType = stream->readUint32BE();
99
100 if ((formType != 0 && fileFormType != formType) ||
101 (formType == 0 && fileFormType != ID_IFZS && fileFormType != ID_IFSF))
102 return false;
103
104 if ((int)size > stream->size() || (size & 1) || (size < 4))
105 return false;
106 size -= 4;
107
108 // Iterate through reading chunk headers
109 while (size > 0) {
110 if (size < 8)
111 // Couldn't contain a chunk
112 return false;
113
114 // Get in the chunk header
115 Chunk c;
116 c._id = stream->readUint32BE();
117 c._size = stream->readUint32BE();
118 c._offset = stream->pos();
119 _chunks.push_back(c);
120
121 int chunkRemainder = c._size + (c._size & 1);
122 if ((stream->pos() + chunkRemainder) > stream->size())
123 // Chunk goes beyond the file size
124 return false;
125
126 size -= 8 + chunkRemainder;
127 stream->skip(chunkRemainder);
128 }
129
130 return true;
131 }
132
getSavegameDescription(Common::SeekableReadStream * rs,Common::String & saveName)133 bool QuetzalReader::getSavegameDescription(Common::SeekableReadStream *rs, Common::String &saveName) {
134 QuetzalReader r;
135 if (!r.open(rs, 0))
136 return false;
137
138 for (Iterator it = r.begin(); it != r.end(); ++it) {
139 if ((*it)._id == ID_ANNO) {
140 Common::SeekableReadStream *s = it.getStream();
141 saveName = readString(s);
142 delete s;
143
144 return true;
145 }
146 }
147
148 saveName = _("Untitled Savegame");
149 return true;
150 }
151
getSavegameMetaInfo(Common::SeekableReadStream * rs,SaveStateDescriptor & ssd)152 bool QuetzalReader::getSavegameMetaInfo(Common::SeekableReadStream *rs, SaveStateDescriptor &ssd) {
153 QuetzalReader r;
154 if (!r.open(rs, 0))
155 return false;
156
157 ssd.setDescription(_("Untitled Savegame"));
158
159 for (Iterator it = r.begin(); it != r.end(); ++it) {
160 if ((*it)._id == ID_ANNO) {
161 Common::SeekableReadStream *s = it.getStream();
162 ssd.setDescription(readString(s));
163 delete s;
164
165 } else if ((*it)._id == ID_SCVM) {
166 Common::SeekableReadStream *s = it.getStream();
167 int year = s->readUint16BE();
168 int month = s->readUint16BE();
169 int day = s->readUint16BE();
170 int hour = s->readUint16BE();
171 int minute = s->readUint16BE();
172 uint32 playTime = s->readUint32BE();
173 delete s;
174
175 ssd.setSaveDate(year, month, day);
176 ssd.setSaveTime(hour, minute);
177 ssd.setPlayTime(playTime);
178 }
179 }
180
181 return true;
182 }
183
readString(Common::ReadStream * src)184 Common::String QuetzalReader::readString(Common::ReadStream *src) {
185 char c;
186 Common::String result;
187 while ((c = src->readByte()) != 0)
188 result += c;
189
190 return result;
191 }
192
193 /*--------------------------------------------------------------------------*/
194
add(uint32 chunkId)195 Common::WriteStream &QuetzalWriter::add(uint32 chunkId) {
196 // Sanity check to prevent adding the same chunk multiple times
197 for (uint idx = 0; idx < _chunks.size(); ++idx) {
198 if (_chunks[idx]._id == chunkId)
199 error("Duplicate chunk added");
200 }
201
202 _chunks.push_back(Chunk(chunkId));
203 return _chunks.back()._stream;
204 }
205
save(Common::WriteStream * out,const Common::String & saveName,uint32 formType)206 void QuetzalWriter::save(Common::WriteStream *out, const Common::String &saveName, uint32 formType) {
207 // Add chunks common to all Glk savegames
208 addCommonChunks(saveName);
209
210 // Calculate the size of the chunks
211 uint size = 4;
212 for (uint idx = 0; idx < _chunks.size(); ++idx)
213 size += 8 + _chunks[idx]._stream.size() + (_chunks[idx]._stream.size() & 1);
214
215 // Write out the header
216 out->writeUint32BE(ID_FORM);
217 out->writeUint32BE(size);
218 out->writeUint32BE(formType);
219
220 // Loop through writing the chunks
221 for (uint idx = 0; idx < _chunks.size(); ++idx) {
222 Common::MemoryWriteStreamDynamic &s = _chunks[idx]._stream;
223
224 out->writeUint32BE(_chunks[idx]._id);
225 out->writeUint32BE(s.size());
226 out->write(s.getData(), s.size());
227 if (s.size() & 1)
228 out->writeByte(0);
229 }
230 }
231
addCommonChunks(const Common::String & saveName)232 void QuetzalWriter::addCommonChunks(const Common::String &saveName) {
233 // Write 'ANNO' chunk
234 {
235 Common::WriteStream &ws = add(ID_ANNO);
236 ws.write(saveName.c_str(), saveName.size());
237 ws.writeByte(0);
238 }
239
240 // Write 'SCVM' chunk with game version & gameplay statistics
241 {
242 Common::WriteStream &ws = add(ID_SCVM);
243
244 // Write out the save date/time
245 TimeDate td;
246 g_system->getTimeAndDate(td);
247 ws.writeSint16BE(td.tm_year + 1900);
248 ws.writeSint16BE(td.tm_mon + 1);
249 ws.writeSint16BE(td.tm_mday);
250 ws.writeSint16BE(td.tm_hour);
251 ws.writeSint16BE(td.tm_min);
252 ws.writeUint32BE(g_vm->_events->getTotalPlayTicks());
253
254 // Write out intrepreter type, language, and game Id
255 ws.writeUint32BE(getInterpreterTag(g_vm->getInterpreterType()));
256 const char *langCode = getLanguageCode(g_vm->getLanguage());
257 if (langCode)
258 ws.write(langCode, strlen(langCode) + 1);
259 else
260 ws.writeByte('\0');
261
262 Common::String md5 = g_vm->getGameMD5();
263 ws.write(md5.c_str(), md5.size());
264 ws.writeByte('\0');
265 }
266 }
267
268 } // End of namespace Glk
269