/* 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 .
*
*/
#include "agi/agi.h"
#include "agi/disk_image.h"
#include "agi/loader.h"
#include "agi/words.h"
#include "common/fs.h"
namespace Agi {
// GalLoader reads KQ1 PC Booter floppy disk images.
//
// 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.
//
// All KQ1 PC booter versions are only one disk.
//
// The disks do not use a standard file system. Instead, file locations are
// stored in a directory structure 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
// disk image file. The only naming requirement is that the image has a known
// file extension.
//
// AgiMetaEngineDetection also scans for usable disk images. It finds and hashes
// the logic directory inside the disk, and matches against the detection table.
/**
* Locates the disk image and the disk offset of the resource directory
*/
void GalLoader::init() {
// build sorted array of files with image extensions
Common::Array imageFiles;
FileMap fileMap;
getPotentialDiskImages(pcDiskImageExtensions, ARRAYSIZE(pcDiskImageExtensions), imageFiles, fileMap);
// find the disk by reading potential images until successful
for (uint i = 0; i < imageFiles.size(); i++) {
const Common::Path &imageFile = imageFiles[i];
Common::SeekableReadStream *stream = openPCDiskImage(imageFile, fileMap[imageFile]);
if (stream == nullptr) {
continue;
}
// look for the directory in both locations
if (isDirectory(*stream, GAL_DIR_POSITION_PCJR)) {
_imageFile = imageFile.baseName();
_dirOffset = GAL_DIR_POSITION_PCJR;
} else if (isDirectory(*stream, GAL_DIR_POSITION_PC)) {
_imageFile = imageFile.baseName();
_dirOffset = GAL_DIR_POSITION_PC;
}
delete stream;
if (!_imageFile.empty()) {
break;
}
}
if (_imageFile.empty()) {
warning("GalLoader: disk not found");
}
}
/**
* Identifies the directory by validating the first few logic entries
*/
bool GalLoader::isDirectory(Common::SeekableReadStream &stream, uint32 dirOffset) {
for (int i = 0; i < 10; i++) {
stream.seek(dirOffset + (i * 4));
uint32 sectorCount;
uint32 logicOffset = readDirectoryEntry(stream, §orCount);
stream.seek(logicOffset);
uint32 logicSize = 8;
for (int j = 0; j < 4; j++) {
logicSize += stream.readUint16LE();
}
if (stream.eos()) {
return false;
}
stream.seek(logicOffset + logicSize - 1);
byte logicTerminator = stream.readByte();
if (stream.eos() || logicTerminator != 0xff) {
return false;
}
}
return true;
}
/**
* Reads a directory entry.
*
* Returns the disk offset and the resource size in sectors.
*/
uint32 GalLoader::readDirectoryEntry(Common::SeekableReadStream &stream, uint32 *sectorCount) {
// 9 bit offset (last bit is MSB)
// 6 bit zero
// 5 bit sector count
// 2 bit zero
// 10 bit sector
byte b0 = stream.readByte();
byte b1 = stream.readByte();
byte b2 = stream.readByte();
byte b3 = stream.readByte();
uint16 offset = ((b1 & 0x80) << 1) | b0;
uint16 sector = ((b2 & 0x03) << 8) | b3;
*sectorCount = ((b1 & 0x01) << 4) | (b2 >> 4);
return (sector * 512) + offset;
}
int GalLoader::loadDirs() {
// if init didn't find disk then fail
if (_imageFile.empty()) {
return errFilesNotFound;
}
// open disk
Common::File disk;
if (!disk.open(Common::Path(_imageFile))) {
return errBadFileOpen;
}
// load logic and picture directory entries.
// pictures do not have directory entries. each picture immediately follows
// its logic. if there is no real picture then it is just the FF terminator.
uint32 sectorCount;
for (int i = 0; i < 84; i++) {
disk.seek(_dirOffset + (i * 4));
uint32 logicOffset = readDirectoryEntry(disk, §orCount);
// seek to logic and calculate length from header
disk.seek(logicOffset);
uint32 logicLength = 8;
for (int j = 0; j < 4; j++) {
logicLength += disk.readUint16LE();
}
if (disk.eos()) {
return errBadResource;
}
// scan for picture terminator after logic
uint32 pictureOffset = logicOffset + logicLength;
disk.seek(pictureOffset);
uint32 pictureLength = 0;
while (true) {
byte terminator = disk.readByte();
if (disk.eos()) {
return errBadResource;
}
if (terminator == 0xff) {
pictureLength = disk.pos() - pictureOffset;
break;
}
}
_vm->_game.dirLogic[i].offset = logicOffset;
_vm->_game.dirLogic[i].len = logicLength;
_vm->_game.dirPic[i].offset = pictureOffset;
_vm->_game.dirPic[i].len = pictureLength;
}
// load sound directory entries
for (int i = 0; i < 10; i++) {
disk.seek(_dirOffset + ((90 + i) * 4));
uint32 soundOffset = readDirectoryEntry(disk, §orCount);
// seek to sound and calculate length from header
disk.seek(soundOffset);
uint32 soundLength = 8;
for (int j = 0; j < 4; j++) {
soundLength += disk.readUint16LE();
}
if (disk.eos()) {
return errBadResource;
}
_vm->_game.dirSound[i].offset = soundOffset;
_vm->_game.dirSound[i].len = soundLength;
}
// load view directory entries
for (int i = 0; i < 110; i++) {
disk.seek(_dirOffset + ((128 + i) * 4));
uint32 viewOffset = readDirectoryEntry(disk, §orCount);
// seek to view and calculate length from header
disk.seek(viewOffset);
uint32 viewLength = 2 + disk.readUint16LE();
if (disk.eos()) {
return errBadResource;
}
_vm->_game.dirView[i].offset = viewOffset;
_vm->_game.dirView[i].len = viewLength;
}
return errOK;
}
uint8 *GalLoader::loadVolumeResource(AgiDir *agid) {
Common::File disk;
if (!disk.open(Common::Path(_imageFile))) {
warning("GalLoader: unable to open disk image: %s", _imageFile.c_str());
return nullptr;
}
// read resource
uint8 *data = (uint8 *)calloc(1, agid->len);
disk.seek(agid->offset);
if (disk.read(data, agid->len) != agid->len) {
warning("GalLoader: error reading %d bytes at offset %d", agid->len, agid->offset);
free(data);
return nullptr;
}
return data;
}
// TODO
int GalLoader::loadObjects() {
return errOK;
}
// TODO
int GalLoader::loadWords() {
return errOK;
}
} // End of namespace Agi