File: loader_v1.cpp

package info (click to toggle)
scummvm 2.9.1%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 450,580 kB
  • sloc: cpp: 4,299,825; asm: 28,322; python: 12,901; sh: 11,302; java: 9,289; xml: 7,895; perl: 2,639; ansic: 2,465; yacc: 1,670; javascript: 1,020; makefile: 933; lex: 578; awk: 275; objc: 82; sed: 11; php: 1
file content (388 lines) | stat: -rw-r--r-- 12,638 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
/* ScummVM - Graphic Adventure Engine
 *
 * ScummVM is the legal property of its developers, whose names
 * are too numerous to list here. Please refer to the COPYRIGHT
 * file distributed with this source distribution.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include "agi/agi.h"
#include "agi/disk_image.h"
#include "agi/loader.h"
#include "agi/words.h"

#include "common/fs.h"

namespace Agi {

// AgiLoader_v1 reads PC Booter floppy disk images.
//
// - King's Quest II           V1      2 disks
// - The Black Cauldron        V1      2 disks
// - Donald Duck's Playground  V2.001  1 disk
//
// All disks are 360k. The only supported image format is "raw". There are no
// headers, footers, or metadata. Each image file must be exactly 368,640 bytes.
//
// The disks do not use a standard file system. Instead, file locations are
// stored in an INITDIR structure at a fixed location. The interpreter version
// determines the location and format of INITDIR.
//
// File detection is done a little differently. Instead of requiring hard-coded
// names for the image files, we scan the game directory for the first usable
// image of disk one, and then scan for disk two. The only naming requirement is
// that the images have a known file extension.
//
// AgiMetaEngineDetection also scans for usable disk images. It finds the LOGDIR
// file inside disk one, hashes LOGDIR, and matches against the detection table.

void AgiLoader_v1::init() {
	// build sorted array of files with image extensions
	Common::Array<Common::Path> imageFiles;
	FileMap fileMap;
	getPotentialDiskImages(pcDiskImageExtensions, ARRAYSIZE(pcDiskImageExtensions), imageFiles, fileMap);

	// find disk one by reading potential images until successful
	uint diskOneIndex;
	for (diskOneIndex = 0; diskOneIndex < imageFiles.size(); diskOneIndex++) {
		const Common::Path &imageFile = imageFiles[diskOneIndex];
		Common::SeekableReadStream *stream = openPCDiskImage(imageFile, fileMap[imageFile]);
		if (stream == nullptr) {
			continue;
		}

		// read image as disk one
		bool success;
		int vol0Offset = 0;
		if (_vm->getVersion() < 0x2001) {
			success = readDiskOneV1(*stream);
		} else {
			success = readDiskOneV2001(*stream, vol0Offset);
		}
		delete stream;

		if (success) {
			debugC(3, "AgiLoader_v1: disk one found: %s", imageFile.baseName().c_str());
			_imageFiles.push_back(imageFile.baseName());
			if (_vm->getVersion() < 0x2001) {
				// the first disk contains volumes 0 and 1.
				// there is no volume offset, resource
				// directories use absolute disk positions.
				_volumes.push_back(AgiDiskVolume(0, 0));
				_volumes.push_back(AgiDiskVolume(0, 0));
			} else {
				// the first disk contains volume 0.
				// resource offsets are relative to its location.
				_volumes.push_back(AgiDiskVolume(0, vol0Offset));
			}
			break;
		}
	}

	// if disk one wasn't found, we're done
	if (_imageFiles.empty()) {
		warning("AgiLoader_v1: disk one not found");
		return;
	}

	// two games have a second disk
	if (!(_vm->getGameID() == GID_KQ2 || _vm->getGameID() == GID_BC)) {
		return;
	}

	// find disk two by locating the next image file that begins with a resource
	// header with a volume number set to two. since the potential image file list
	// is sorted, begin with the file after disk one and try until successful.
	for (uint i = 1; i < imageFiles.size(); i++) {
		uint diskTwoIndex = (diskOneIndex + i) % imageFiles.size();
		Common::Path &imageFile = imageFiles[diskTwoIndex];

		Common::SeekableReadStream *stream = openPCDiskImage(imageFile, fileMap[imageFile]);
		if (stream == nullptr) {
			continue;
		}

		// read resource header
		uint16 magic = stream->readUint16BE();
		byte volume = stream->readByte();
		delete stream;

		if (magic == 0x1234 && volume == 2) {
			debugC(3, "AgiLoader_v1: disk two found: %s", imageFile.baseName().c_str());
			_imageFiles.push_back(imageFile.baseName());
			_volumes.push_back(AgiDiskVolume(_imageFiles.size() - 1, 0));
			break;
		}
	}

	if (imageFiles.size() < 2) {
		warning("AviLoader_v1: disk two not found");
	}
}

bool AgiLoader_v1::readDiskOneV1(Common::SeekableReadStream &stream) {
	// INITDIR V1 is located at the 9th sector after the 5-byte resource header.
	// Each entry is 10 bytes and there are always 8.
	stream.seek(PC_INITDIR_POSITION_V1);
	uint16 magic = stream.readUint16BE();
	byte volume = stream.readByte();
	uint16 size = stream.readUint16LE();
	if (!(magic == 0x1234 && volume == 1 && size == PC_INITDIR_SIZE_V1)) {
		return false;
	}

	bool success = true;
	success &= readInitDirV1(stream, PC_INITDIR_LOGDIR_INDEX_V1,   _logDir);
	success &= readInitDirV1(stream, PC_INITDIR_PICDIR_INDEX_V1,   _picDir);
	success &= readInitDirV1(stream, PC_INITDIR_VIEWDIR_INDEX_V1,  _viewDir);
	success &= readInitDirV1(stream, PC_INITDIR_SOUNDDIR_INDEX_V1, _soundDir);
	success &= readInitDirV1(stream, PC_INITDIR_OBJECTS_INDEX_V1,  _objects);
	success &= readInitDirV1(stream, PC_INITDIR_WORDS_INDEX_V1,    _words);
	return success;
}

bool AgiLoader_v1::readInitDirV1(Common::SeekableReadStream &stream, byte index, AgiDir &agid) {
	// read INITDIR entry
	stream.seek(PC_INITDIR_POSITION_V1 + 5 + (index * PC_INITDIR_ENTRY_SIZE_V1));
	byte volume = stream.readByte();
	byte head = stream.readByte();
	uint16 track = stream.readUint16LE();
	uint16 sector = stream.readUint16LE();
	uint16 offset = stream.readUint16LE();
	if (stream.eos() || stream.err()) {
		return false;
	}

	// resource must be on disk one
	if (!(volume == 0 || volume == 1)) {
		return false;
	}

	// resource begins with a 5-byte header
	uint32 position = PC_DISK_POSITION(head, track, sector, offset);
	stream.seek(position);
	uint16 magic = stream.readUint16BE();
	volume = stream.readByte();
	uint16 size = stream.readUint16LE();
	if (!(magic == 0x1234 && (volume == 0 || volume == 1))) {
		return false;
	}
	if (!(stream.pos() + size <= stream.size())) {
		return false;
	}

	// resource found
	agid.volume = volume;
	agid.offset = stream.pos();
	agid.len = size;
	agid.clen = size;
	return true;
}

bool AgiLoader_v1::readDiskOneV2001(Common::SeekableReadStream &stream, int &vol0Offset) {
	// INITDIR V2001 is located at the 2nd sector with no resource header.
	// Each entry is 3 bytes. The number of entries is technically variable,
	// because the list ends in an entry for each volume followed by FF FF FF.
	// But since there was only one V2001 game (Donald Duck's Playground),
	// and it only has one disk, there is really only ever one volume.

	bool success = true;
	success &= readInitDirV2001(stream, PC_INITDIR_LOGDIR_INDEX_V2001,   _logDir);
	success &= readInitDirV2001(stream, PC_INITDIR_PICDIR_INDEX_V2001,   _picDir);
	success &= readInitDirV2001(stream, PC_INITDIR_VIEWDIR_INDEX_V2001,  _viewDir);
	success &= readInitDirV2001(stream, PC_INITDIR_SOUNDDIR_INDEX_V2001, _soundDir);
	success &= readInitDirV2001(stream, PC_INITDIR_OBJECTS_INDEX_V2001,  _objects);
	success &= readInitDirV2001(stream, PC_INITDIR_WORDS_INDEX_V2001,    _words);

	// V2001 directories (LOGDIR, etc) contain resource offsets relative to
	// the start of their volume on disk. All volumes start at the beginning
	// of the disk, except for volume 0.
	AgiDir vol0;
	success &= readInitDirV2001(stream, PC_INITDIR_VOL0_INDEX_V2001, vol0);
	vol0Offset = vol0.offset - 5;

	return success;
}

bool AgiLoader_v1::readInitDirV2001(Common::SeekableReadStream &stream, byte index, AgiDir &agid) {
	// read INITDIR entry
	stream.seek(PC_INITDIR_POSITION_V2001 + (index * PC_INITDIR_ENTRY_SIZE_V2001));
	byte b0 = stream.readByte();
	byte b1 = stream.readByte();

	// volume      4 bits
	// position   12 bits  (in half-sectors)
	byte volume = b0 >> 4;
	uint32 position = (((b0 & 0x0f) << 8) + b1) * 256;

	// resource must be on disk one (because the only V2001 game is one disk)
	if (!(volume == 0 || volume == 1)) {
		return false;
	}

	// resource begins with a 5-byte header
	stream.seek(position);
	uint16 magic = stream.readUint16BE();
	volume = stream.readByte();
	uint16 size = stream.readUint16LE();
	if (!(magic == 0x1234 && (volume == 0 || volume == 1))) {
		return false;
	}
	if (!(stream.pos() + size <= stream.size())) {
		return false;
	}

	// resource found
	agid.volume = volume;
	agid.offset = stream.pos();
	agid.len = size;
	agid.clen = size;
	return true;
}

int AgiLoader_v1::loadDirs() {
	// if init didn't find disks then fail
	if (_imageFiles.empty()) {
		return errFilesNotFound;
	}

	// open disk one
	Common::File disk;
	if (!disk.open(Common::Path(_imageFiles[0]))) {
		return errBadFileOpen;
	}

	// load each directory
	bool success = true;
	success &= loadDir(_vm->_game.dirLogic, disk, _logDir.offset,   _logDir.len);
	success &= loadDir(_vm->_game.dirPic,   disk, _picDir.offset,   _picDir.len);
	success &= loadDir(_vm->_game.dirView,  disk, _viewDir.offset,  _viewDir.len);
	success &= loadDir(_vm->_game.dirSound, disk, _soundDir.offset, _soundDir.len);
	return success ? errOK : errBadResource;
}

bool AgiLoader_v1::loadDir(AgiDir *dir, Common::File &disk, uint32 dirOffset, uint32 dirLength) {
	// seek to directory on disk
	disk.seek(dirOffset);

	// re-validate length from initdir
	if (!(disk.pos() + dirLength <= disk.size())) {
		return false;
	}

	// read directory entries
	uint16 dirEntryCount = MIN<uint32>(dirLength / 3, MAX_DIRECTORY_ENTRIES);
	for (uint16 i = 0; i < dirEntryCount; i++) {
		byte b0 = disk.readByte();
		byte b1 = disk.readByte();
		byte b2 = disk.readByte();
		if (b0 == 0xff && b1 == 0xff && b2 == 0xff) {
			continue;
		}

		if (_vm->getVersion() < 0x2001) {
			// volume   2 bits
			// track    6 bits
			// sector   6 bits (one based)
			// head     1 bit
			// offset   9 bits
			dir[i].volume = b0 >> 6;
			byte track = b0 & 0x3f;
			byte sector = b1 >> 2;
			byte head = (b1 >> 1) & 1;
			uint16 offset = ((b1 & 1) << 8) | b2;
			dir[i].offset = PC_DISK_POSITION(head, track, sector, offset);
		} else {
			// volume   4 bits
			// sector  11 bits (zero based)
			// offset   9 bits
			// position is relative to the start of volume
			dir[i].volume = b0 >> 4;
			uint16 sector = ((b0 & 0x0f) << 7) | (b1 >> 1);
			uint16 offset = ((b1 & 0x01) << 8) | b2;
			dir[i].offset = PC_DISK_POSITION(0, 0, sector + 1, offset);
		}
	}

	return true;
}

uint8 *AgiLoader_v1::loadVolumeResource(AgiDir *agid) {
	if (agid->volume >= _volumes.size()) {
		warning("AgiLoader_v1: invalid volume: %d", agid->volume);
		return nullptr;
	}

	Common::File disk;
	int diskIndex = _volumes[agid->volume].disk;
	if (!disk.open(Common::Path(_imageFiles[diskIndex]))) {
		warning("AgiLoader_v1: unable to open disk image: %s", _imageFiles[diskIndex].c_str());
		return nullptr;
	}

	// seek to resource and validate header
	int offset = _volumes[agid->volume].offset + agid->offset;
	disk.seek(offset);
	uint16 magic = disk.readUint16BE();
	if (magic != 0x1234) {
		warning("AgiLoader_v1: no resource at volume %d offset %d", agid->volume, agid->offset);
		return nullptr;
	}
	disk.skip(1); // volume
	agid->len = disk.readUint16LE();

	uint8 *data = (uint8 *)calloc(1, agid->len + 32); // why the extra 32 bytes?
	if (disk.read(data, agid->len) != agid->len) {
		warning("AgiLoader_v1: error reading %d bytes at volume %d offset %d", agid->len, agid->volume, agid->offset);
		free(data);
		return nullptr;
	}

	return data;
}

int AgiLoader_v1::loadObjects() {
	// DDP has an empty-ish objects resource but doesn't use it
	if (_vm->getGameID() == GID_DDP) {
		return errOK;
	}

	Common::File disk;
	if (!disk.open(Common::Path(_imageFiles[0]))) {
		return errBadFileOpen;
	}

	disk.seek(_objects.offset);
	return _vm->loadObjects(disk, _objects.len);
}

int AgiLoader_v1::loadWords() {
	// DDP has an empty-ish words resource but doesn't use it
	if (_vm->getGameID() == GID_DDP) {
		return errOK;
	}

	Common::File disk;
	if (!disk.open(Common::Path(_imageFiles[0]))) {
		return errBadFileOpen;
	}

	// TODO: pass length and validate in parser
	disk.seek(_words.offset);
	return _vm->_words->loadDictionary_v1(disk);
}

} // End of namespace Agi