File: loader_gal_a2.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 (297 lines) | stat: -rw-r--r-- 8,907 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
/* 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 {

// GalLoader_A2 reads KQ1 Apple II floppy disk images.
//
// Floppy disks have two sides; each side is a disk with its own image file.
// All disk sides are 140k with 35 tracks and 16 sectors per track.
//
// KQ1 has three disk sides (labeled A, B, C) on two physical disks.
//
// Multiple disk image formats are supported; see Common::DiskImage. The file
// extension determines the format. For example: .do, .dsk, .nib, .woz.
//
// The disks do not use a standard file system. Instead, file locations are
// stored in directory structures at known locations.
//
// 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 continue scanning until all disks are found.
// The directory from disk one is used to identify each disk by its content.
// The only naming requirement is that the images have a known file extension.
//
// AgiMetaEngineDetection also scans for usable disk images. It finds and hashes
// the logic directory inside disk one, and matches against the detection table.

GalLoader_A2::~GalLoader_A2() {
	for (uint d = 0; d < _disks.size(); d++) {
		delete _disks[d];
	}
}

/**
 * Locates the three disk images.
 */
void GalLoader_A2::init() {
	// build sorted array of files with image extensions
	Common::Array<Common::Path> imageFiles;
	FileMap fileMap;
	getPotentialDiskImages(a2DiskImageExtensions, ARRAYSIZE(a2DiskImageExtensions), imageFiles, fileMap);

	// find disk one by reading potential images until successful
	_disks.clear();
	AgiDir logicDir[GAL_A2_LOGIC_COUNT];
	uint diskOneIndex;
	for (diskOneIndex = 0; diskOneIndex < imageFiles.size(); diskOneIndex++) {
		const Common::Path &imageFile = imageFiles[diskOneIndex];
		Common::SeekableReadStream *stream = openA2DiskImage(imageFile, fileMap[imageFile]);
		if (stream == nullptr) {
			warning("GalLoader_A2: unable to open disk image: %s", imageFile.baseName().c_str());
			continue;
		}

		// read image as disk one
		if (readDiskOne(*stream, logicDir)) {
			debugC(3, "GalLoader_A2: disk one found: %s", imageFile.baseName().c_str());
			_disks.resize(GAL_A2_DISK_COUNT);
			_disks[0] = stream;
			break;
		} else {
			delete stream;
		}
	}

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

	// find all other disks by comparing their contents to the logic directory.
	int disksFound = 1;
	for (uint i = 1; i < imageFiles.size() && disksFound < GAL_A2_DISK_COUNT; i++) {
		uint imageFileIndex = (diskOneIndex + i) % imageFiles.size();
		Common::Path &imageFile = imageFiles[imageFileIndex];

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

		// check each disk
		bool diskFound = false;
		for (int d = 1; d < GAL_A2_DISK_COUNT; d++) {
			// has disk already been found?
			if (_disks[d] != nullptr) {
				continue;
			}

			if (validateDisk(*stream, d, logicDir)) {
				_disks[d] = stream;
				disksFound++;
				diskFound = true;
				break;
			}
		}

		if (!diskFound) {
			delete stream;
		}
	}
}

/**
 * Reads a disk image as disk one by attempting to parse the logic directory
 * and then validating that all the expected logic resources exist.
 */
bool GalLoader_A2::readDiskOne(Common::SeekableReadStream &disk, AgiDir *logicDir) {
	disk.seek(GAL_A2_LOGDIR_POSITION);

	// attempt to read logic directory
	for (int i = 0; i < GAL_A2_LOGIC_COUNT; i++) {
		if (!readDirectoryEntry(disk, logicDir[i])) {
			return false;
		}
	}

	// validate that all disk one logics exist
	return validateDisk(disk, 0, logicDir);
}

/**
 * Reads a directory entry.
 */
bool GalLoader_A2::readDirectoryEntry(Common::SeekableReadStream &stream, AgiDir &dirEntry) {
	// GAL A2 DIR format:
	// track   8 bits
	// disk    4 bits (0 for all disks, else 1-3)
	// sector  4 bits
	// offset  8 bits
	byte b0 = stream.readByte();
	byte b1 = stream.readByte();
	byte b2 = stream.readByte();

	byte disk = b1 >> 4;
	byte sector = b1 & 0x0f;
	uint32 position = A2_DISK_POSITION(b0, sector, b2);

	// use the first disk for resources that are on all disks
	if (disk > 0) {
		disk--;
	}

	// validate entry
	if (!(disk <= 2 && position < A2_DISK_SIZE)) {
		return false;
	}

	dirEntry.volume = disk;
	dirEntry.offset = position;
	return true;
}

/**
 * Tests if a disk contains all of the expected logic resources.
 */
bool GalLoader_A2::validateDisk(Common::SeekableReadStream &disk, byte diskIndex, AgiDir *logicDir) {
	for (int i = 0; i < GAL_A2_LOGIC_COUNT; i++) {
		// Only validate logics on this disk
		if (logicDir[i].volume != diskIndex) {
			continue;
		}

		// Do not use logic 64 to validate a disk. Its logic header contains
		// an incorrect length that is one byte too large. This would fail our
		// validation method below of comparing the resource length in the A2
		// header to the length in the logic header.
		if (i == 64) {
			continue;
		}

		// A2 resources begin with a header consisting of the resource length.
		// Logic resources begin with a header consisting of four lengths; one
		// for each section of the logic. If a logic exists at this location,
		// then the A2 length will equal the calculated logic length.
		disk.seek(logicDir[i].offset);
		uint16 resourceLength = disk.readUint16LE();
		uint32 logicLength = 8;
		for (int j = 0; j < 4; j++) {
			logicLength += disk.readUint16LE();
		}
		if (disk.eos() ||
			resourceLength != logicLength ||
			!(logicDir[i].offset + 2 + resourceLength <= A2_DISK_SIZE)) {
			return false;
		}
	}
	return true;
}

/**
 * Load logic, pic, and view directories. KQ1-A2 has no sound resources.
 */
int GalLoader_A2::loadDirs() {
	// if init didn't find disks then fail
	if (_disks.empty()) {
		return errFilesNotFound;
	}
	for (uint d = 0; d < _disks.size(); d++) {
		if (_disks[d] == nullptr) {
			warning("AgiLoader_A2: disk %d not found", d);
			return errFilesNotFound;
		}
	}

	// directories are on disk one
	Common::SeekableReadStream &disk = *_disks[0];

	bool success = true;
	success &= loadDir(_vm->_game.dirLogic, disk, GAL_A2_LOGDIR_POSITION,  GAL_A2_LOGIC_COUNT);
	success &= loadDir(_vm->_game.dirPic,   disk, GAL_A2_PICDIR_POSITION,  GAL_A2_PICTURE_COUNT);
	success &= loadDir(_vm->_game.dirView,  disk, GAL_A2_VIEWDIR_POSITION, GAL_A2_VIEW_COUNT);
	return success ? errOK : errBadResource;
}

/**
 * Loads a resource directory.
 */
bool GalLoader_A2::loadDir(AgiDir *dir, Common::SeekableReadStream &disk, uint32 dirOffset, uint32 dirCount) {
	disk.seek(dirOffset);
	for (uint32 i = 0; i < dirCount; i++) {
		// Skip pictures 0 and 81. These pictures do not exist, but the entries
		// contain junk bytes. This did not matter in the original because they
		// never loaded, but if we read them then they will fail validation.
		if ((i == 0 || i == 81) && dirOffset == GAL_A2_PICDIR_POSITION) {
			disk.skip(3);
			continue;
		}

		if (!readDirectoryEntry(disk, dir[i])) {
			return false;
		}
	}
	return true;
}

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

	Common::SeekableReadStream &disk = *_disks[agid->volume];

	// seek to resource and read header (resource length)
	disk.seek(agid->offset);
	agid->len = disk.readUint16LE();

	uint8 *data = (uint8 *)calloc(1, agid->len);
	if (disk.read(data, agid->len) != agid->len) {
		warning("GalLoader_A2: error reading %d bytes at volume %d offset %d", agid->len, agid->volume, agid->offset);
		free(data);
		return nullptr;
	}

	return data;
}

// TODO
int GalLoader_A2::loadObjects() {
	return errOK;
}

// TODO
int GalLoader_A2::loadWords() {
	// words location: GAL_A2_WORDS_POSITION
	// two byte header with resource length.
	return errOK;
}

} // End of namespace Agi