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