AGI: Apple II NIB and WOZ disk image support

This commit is contained in:
sluicebox 2024-08-24 14:40:36 -07:00 committed by Filippos Karapetis
parent 23a9f4ed54
commit 2f2f3a59bd
8 changed files with 195 additions and 95 deletions

View File

@ -587,6 +587,7 @@ protected:
class AgiLoader_A2 : public AgiLoader {
public:
AgiLoader_A2(AgiEngine *vm) : AgiLoader(vm) {}
~AgiLoader_A2() override;
void init() override;
int loadDirs() override;
@ -595,7 +596,7 @@ public:
int loadWords() override;
private:
Common::Array<Common::String> _imageFiles;
Common::Array<Common::SeekableReadStream *> _disks;
Common::Array<AgiDiskVolume> _volumes;
AgiDir _logDir;
AgiDir _picDir;
@ -610,7 +611,7 @@ private:
static bool readVolumeMap(Common::SeekableReadStream &stream, uint32 position, uint32 bufferLength, Common::Array<uint32> &volumeMap);
A2DirVersion detectDirVersion(Common::SeekableReadStream &stream);
bool loadDir(AgiDir *dir, Common::File &disk, uint32 dirOffset, uint32 dirLength, A2DirVersion dirVersion);
bool loadDir(AgiDir *dir, Common::SeekableReadStream &disk, uint32 dirOffset, uint32 dirLength, A2DirVersion dirVersion);
};
class AgiLoader_v1 : public AgiLoader {
@ -933,7 +934,7 @@ public:
// Objects
public:
int loadObjects(const char *fname);
int loadObjects(Common::File &fp, int flen);
int loadObjects(Common::SeekableReadStream &fp, int flen);
const char *objectName(uint16 objectNr);
int objectGetLocation(uint16 objectNr);
void objectSetLocation(uint16 objectNr, int location);

View File

@ -348,6 +348,7 @@ void AgiMetaEngineDetection::getPotentialDiskImages(
if (f->_key.baseName().hasSuffixIgnoreCase(imageExtensions[i])) {
debug(3, "potential disk image: %s", f->_key.baseName().c_str());
imageFiles.push_back(f->_key);
break;
}
}
}
@ -368,16 +369,8 @@ ADDetectedGame AgiMetaEngineDetection::detectPcDiskImageGame(const FileMap &allF
// find disk one by reading potential images until a match is found
for (const Common::Path &imageFile : imageFiles) {
Common::SeekableReadStream *stream = allFiles[imageFile].createReadStream();
Common::SeekableReadStream *stream = openPCDiskImage(imageFile, allFiles[imageFile]);
if (stream == nullptr) {
warning("unable to open disk image: %s", imageFile.baseName().c_str());
continue;
}
// image file size must be 360k
int64 fileSize = stream->size();
if (fileSize != PC_DISK_SIZE) {
delete stream;
continue;
}
@ -409,7 +402,7 @@ ADDetectedGame AgiMetaEngineDetection::detectPcDiskImageGame(const FileMap &allF
FileProperties fileProps;
fileProps.md5 = file->md5;
fileProps.md5prop = kMD5Archive;
fileProps.size = fileSize;
fileProps.size = PC_DISK_SIZE;
detectedGame.matchedFiles[imageFile] = fileProps;
return detectedGame;
}
@ -483,20 +476,16 @@ ADDetectedGame AgiMetaEngineDetection::detectA2DiskImageGame(const FileMap &allF
// find disk one by reading potential images until a match is found
for (const Common::Path &imageFile : imageFiles) {
Common::SeekableReadStream *stream = allFiles[imageFile].createReadStream();
// lazily-load disk image tracks as they're accessed.
// prevents decoding entire disks just to read a few dynamic sectors.
// this would create a significant delay for images in the .woz format.
const bool loadAllTracks = false;
Common::SeekableReadStream *stream = openA2DiskImage(imageFile, allFiles[imageFile], loadAllTracks);
if (stream == nullptr) {
warning("unable to open disk image: %s", imageFile.baseName().c_str());
continue;
}
// image file size must be 140k.
// this simple check will be removed when more image formats are supported.
int64 fileSize = stream->size();
if (fileSize != A2_DISK_SIZE) {
delete stream;
continue;
}
// attempt to locate and hash logdir by reading initdir,
// and also known logdir locations for games without initdir.
Common::String logdirHashInitdir = getLogDirHashFromA2DiskImage(*stream);
@ -532,7 +521,7 @@ ADDetectedGame AgiMetaEngineDetection::detectA2DiskImageGame(const FileMap &allF
FileProperties fileProps;
fileProps.md5 = file->md5;
fileProps.md5prop = kMD5Archive;
fileProps.size = fileSize;
fileProps.size = A2_DISK_SIZE;
detectedGame.matchedFiles[imageFile] = fileProps;
return detectedGame;
}

123
engines/agi/disk_image.cpp Normal file
View File

@ -0,0 +1,123 @@
/* 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 "common/formats/disk_image.h"
#include "common/memstream.h"
#include "common/path.h"
#include "common/textconsole.h"
#include "agi/disk_image.h"
namespace Agi {
/**
* DiskImageStream is a stream wrapper around Common::DiskImage.
*
* This allows DiskImage to lazily decode tracks as a stream is used.
* This is important for detection, because the .woz format is noticeably
* expensive to decode all tracks at once, and detection has to read
* INITDIR to discover which track to read LOGDIR from.
*/
class DiskImageStream : virtual public Common::SeekableReadStream {
public:
DiskImageStream(Common::DiskImage *diskImage) : _diskImage(diskImage), _stream(_diskImage->getDiskStream()) {}
~DiskImageStream() {
delete _diskImage;
}
uint32 read(void *dataPtr, uint32 dataSize) override {
return _diskImage->read(dataPtr, pos(), dataSize);
}
bool eos() const { return _stream->eos(); }
void clearErr() { _stream->clearErr(); }
int64 pos() const { return _stream->pos(); }
int64 size() const { return _stream->size(); }
bool seek(int64 offs, int whence = SEEK_SET) { return _stream->seek(offs, whence); }
private:
Common::DiskImage *_diskImage;
Common::SeekableReadStream *_stream;
};
Common::SeekableReadStream *openPCDiskImage(const Common::Path &path, const Common::FSNode &node) {
Common::SeekableReadStream *stream = node.createReadStream();
if (stream == nullptr) {
warning("unable to open disk image: %s", path.baseName().c_str());
return nullptr;
}
// validate disk size
if (stream->size() != PC_DISK_SIZE) {
delete stream;
return nullptr;
}
return stream;
}
Common::SeekableReadStream *openA2DiskImage(const Common::Path &path, const Common::FSNode &node, bool loadAllTracks) {
Common::String name = path.baseName();
// Open the image with Common::DiskImage, unless the file extension is ".img".
// DiskImage expects ".img" to be a PC disk image, but it also gets used as
// an Apple II raw sector disk image, so just open it and and read it.
Common::SeekableReadStream *stream = nullptr;
if (name.hasSuffixIgnoreCase(".img")) {
stream = node.createReadStream();
} else {
if (loadAllTracks) {
// when loading all tracks, open with DiskImage and take the stream.
Common::DiskImage diskImage;
if (diskImage.open(node)) {
stream = diskImage.releaseStream();
}
} else {
// when loading tracks as they're used, create a DiskImage with lazy
// decoding and wrap it in a stream.
Common::DiskImage *diskImage = new Common::DiskImage();
diskImage->setLazyDecoding(true);
if (diskImage->open(node)) {
stream = new DiskImageStream(diskImage);
} else {
delete diskImage;
}
}
}
if (stream == nullptr) {
warning("unable to open disk image: %s", path.baseName().c_str());
return nullptr;
}
// validate disk size
if (stream->size() != A2_DISK_SIZE) {
delete stream;
return nullptr;
}
return stream;
}
} // End of namespace Agi

View File

@ -22,6 +22,11 @@
#ifndef AGI_DISK_IMAGE_H
#define AGI_DISK_IMAGE_H
namespace Common {
class SeekableReadStream;
class Path;
}
namespace Agi {
// PC disk image values and helpers for AgiLoader_v1 and AgiMetaEngineDetection
@ -57,7 +62,7 @@ static const char * const pcDiskImageExtensions[] = { ".ima", ".img" };
// A2 disk image values and helpers for AgiLoader_A2 and AgiMetaEngineDetection
// Disk image detection requires that image files have a known extension
static const char * const a2DiskImageExtensions[] = { ".do", ".dsk" };
static const char * const a2DiskImageExtensions[] = { ".do", ".dsk", ".img", ".nib", ".woz" };
#define A2_DISK_SIZE (35 * 16 * 256)
#define A2_DISK_POSITION(t, s, o) ((((t * 16) + s) * 256) + o)
@ -93,6 +98,9 @@ static const char * const a2DiskImageExtensions[] = { ".do", ".dsk" };
#define A2_BC_DISK_COUNT 5
#define A2_BC_VOLUME_COUNT 9
Common::SeekableReadStream *openPCDiskImage(const Common::Path &path, const Common::FSNode &node);
Common::SeekableReadStream *openA2DiskImage(const Common::Path &path, const Common::FSNode &node, bool loadAllTracks = true);
} // End of namespace Agi
#endif /* AGI_DISK_IMAGE_H */

View File

@ -24,7 +24,9 @@
#include "agi/words.h"
#include "common/config-manager.h"
#include "common/formats/disk_image.h"
#include "common/fs.h"
#include "common/memstream.h"
namespace Agi {
@ -33,9 +35,8 @@ namespace Agi {
// 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.
//
// Currently, the only supported image format is "raw", with sectors in logical
// order. Each image file must be exactly 143,360 bytes.
// TODO: Add support for other image formats with ADL's disk iamge code.
// 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 an INITDIR structure at a fixed location. KQ2 and BC don't have
@ -59,6 +60,12 @@ namespace Agi {
typedef Common::HashMap<Common::Path, Common::FSNode, Common::Path::IgnoreCase_Hash, Common::Path::IgnoreCase_EqualTo> FileMap;
AgiLoader_A2::~AgiLoader_A2() {
for (uint d = 0; d < _disks.size(); d++) {
delete _disks[d];
}
}
void AgiLoader_A2::init() {
// get all files in game directory
Common::FSList allFiles;
@ -77,6 +84,7 @@ void AgiLoader_A2::init() {
Common::Path path = file.getPath();
imageFiles.push_back(path);
fileMap[path] = file;
break;
}
}
}
@ -90,28 +98,21 @@ void AgiLoader_A2::init() {
uint diskOneIndex;
for (diskOneIndex = 0; diskOneIndex < imageFiles.size(); diskOneIndex++) {
const Common::Path &imageFile = imageFiles[diskOneIndex];
Common::SeekableReadStream *stream = fileMap[imageFile].createReadStream();
Common::SeekableReadStream *stream = openA2DiskImage(imageFile, fileMap[imageFile]);
if (stream == nullptr) {
warning("AgiLoader_A2: unable to open disk image: %s", imageFile.baseName().c_str());
continue;
}
// image file size must be 140k
int64 fileSize = stream->size();
if (fileSize != A2_DISK_SIZE) {
delete stream;
continue;
}
// read image as disk one
diskCount = readDiskOne(*stream, volumeMap);
delete stream;
if (diskCount > 0) {
debugC(3, "AgiLoader_A2: disk one found: %s", imageFile.baseName().c_str());
_imageFiles.resize(diskCount);
_imageFiles[0] = imageFile.baseName();
_disks.resize(diskCount);
_disks[0] = stream;
break;
} else {
delete stream;
}
}
@ -132,23 +133,16 @@ void AgiLoader_A2::init() {
uint imageFileIndex = (diskOneIndex + i) % imageFiles.size();
Common::Path &imageFile = imageFiles[imageFileIndex];
Common::SeekableReadStream *stream = fileMap[imageFile].createReadStream();
Common::SeekableReadStream *stream = openA2DiskImage(imageFile, fileMap[imageFile]);
if (stream == nullptr) {
warning("AgiLoader_A2: unable to open disk image: %s", imageFile.baseName().c_str());
continue;
}
// image file size must be 140k
int32 fileSize = stream->size();
if (fileSize != A2_DISK_SIZE) {
delete stream;
continue;
}
// check each disk
bool diskFound = false;
for (int d = 1; d < diskCount; d++) {
// has disk already been found?
if (!_imageFiles[d].empty()) {
if (_disks[d] != nullptr) {
continue;
}
@ -173,13 +167,16 @@ void AgiLoader_A2::init() {
}
if (match) {
_imageFiles[d] = imageFile.baseName();
_disks[d] = stream;
disksFound++;
diskFound = true;
break;
}
}
delete stream;
if (!diskFound) {
delete stream;
}
}
// populate _volumes with the locations of the ones we will use.
@ -332,21 +329,18 @@ bool AgiLoader_A2::readVolumeMap(
int AgiLoader_A2::loadDirs() {
// if init didn't find disks then fail
if (_imageFiles.empty()) {
if (_disks.empty()) {
return errFilesNotFound;
}
for (uint32 i = 0; i < _imageFiles.size(); i++) {
if (_imageFiles.empty()) {
warning("AgiLoader_A2: disk %d not found", i);
for (uint d = 0; d < _disks.size(); d++) {
if (_disks[d] == nullptr) {
warning("AgiLoader_A2: disk %d not found", d);
return errFilesNotFound;
}
}
// open disk one
Common::File disk;
if (!disk.open(Common::Path(_imageFiles[0]))) {
return errBadFileOpen;
}
// all dirs are on disk one
Common::SeekableReadStream &disk = *_disks[0];
// detect dir format
A2DirVersion dirVersion = detectDirVersion(disk);
@ -388,7 +382,7 @@ A2DirVersion AgiLoader_A2::detectDirVersion(Common::SeekableReadStream &stream)
return A2DirVersionOld;
}
bool AgiLoader_A2::loadDir(AgiDir *dir, Common::File &disk, uint32 dirOffset, uint32 dirLength, A2DirVersion dirVersion) {
bool AgiLoader_A2::loadDir(AgiDir *dir, Common::SeekableReadStream &disk, uint32 dirOffset, uint32 dirLength, A2DirVersion dirVersion) {
// seek to directory on disk
disk.seek(dirOffset);
@ -440,12 +434,8 @@ uint8 *AgiLoader_A2::loadVolumeResource(AgiDir *agid) {
return nullptr;
}
Common::File disk;
int diskIndex = _volumes[agid->volume].disk;
if (!disk.open(Common::Path(_imageFiles[diskIndex]))) {
warning("AgiLoader_A2: unable to open disk image: %s", _imageFiles[diskIndex].c_str());
return nullptr;
}
Common::SeekableReadStream &disk = *_disks[diskIndex];
// seek to resource and validate header
int offset = _volumes[agid->volume].offset + agid->offset;
@ -469,22 +459,22 @@ uint8 *AgiLoader_A2::loadVolumeResource(AgiDir *agid) {
}
int AgiLoader_A2::loadObjects() {
Common::File disk;
if (!disk.open(Common::Path(_imageFiles[0]))) {
return errBadFileOpen;
if (_disks.empty()) {
return errFilesNotFound;
}
Common::SeekableReadStream &disk = *_disks[0];
disk.seek(_objects.offset);
return _vm->loadObjects(disk, _objects.len);
}
int AgiLoader_A2::loadWords() {
Common::File disk;
if (!disk.open(Common::Path(_imageFiles[0]))) {
return errBadFileOpen;
if (_disks.empty()) {
return errFilesNotFound;
}
// TODO: pass length and validate in parser
Common::SeekableReadStream &disk = *_disks[0];
disk.seek(_words.offset);
if (_vm->getVersion() < 0x2000) {
return _vm->_words->loadDictionary_v1(disk);

View File

@ -80,16 +80,8 @@ void AgiLoader_v1::init() {
uint diskOneIndex;
for (diskOneIndex = 0; diskOneIndex < imageFiles.size(); diskOneIndex++) {
const Common::Path &imageFile = imageFiles[diskOneIndex];
Common::SeekableReadStream *stream = fileMap[imageFile].createReadStream();
Common::SeekableReadStream *stream = openPCDiskImage(imageFile, fileMap[imageFile]);
if (stream == nullptr) {
warning("AgiLoader_v1: unable to open disk image: %s", imageFile.baseName().c_str());
continue;
}
// image file size must be 360k
int32 fileSize = stream->size();
if (fileSize != PC_DISK_SIZE) {
delete stream;
continue;
}
@ -139,26 +131,17 @@ void AgiLoader_v1::init() {
uint diskTwoIndex = (diskOneIndex + i) % imageFiles.size();
Common::Path &imageFile = imageFiles[diskTwoIndex];
Common::SeekableReadStream *stream = fileMap[imageFile].createReadStream();
Common::SeekableReadStream *stream = openPCDiskImage(imageFile, fileMap[imageFile]);
if (stream == nullptr) {
warning("AgiLoader_v1: unable to open disk image: %s", imageFile.baseName().c_str());
continue;
}
// image file size must be 360k
int64 fileSize = stream->size();
if (fileSize != PC_DISK_SIZE) {
delete stream;
continue;
}
// read resource header
uint16 magic = stream->readUint16BE();
byte volume = stream->readByte();
uint16 size = stream->readUint16LE();
delete stream;
if (magic == 0x1234 && volume == 2 && 5 + size <= PC_DISK_SIZE) {
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));

View File

@ -5,6 +5,7 @@ MODULE_OBJS := \
checks.o \
console.o \
cycle.o \
disk_image.o \
font.o \
global.o \
graphics.o \
@ -57,3 +58,10 @@ DETECT_OBJS += $(MODULE)/detection.o
# This is unneeded by the engine module itself,
# so separate it completely.
DETECT_OBJS += $(MODULE)/wagparser.o
# Skip building the following objects if a static
# module is enabled, because it already has the contents.
ifneq ($(ENABLE_AGI), STATIC_PLUGIN)
# External dependencies for detection.
DETECT_OBJS += $(MODULE)/disk_image.o
endif

View File

@ -95,16 +95,14 @@ int AgiEngine::loadObjects(const char *fname) {
* @param fp File pointer
* @param flen File length
*/
int AgiEngine::loadObjects(Common::File &fp, int flen) {
int AgiEngine::loadObjects(Common::SeekableReadStream &fp, int flen) {
uint8 *mem;
if ((mem = (uint8 *)calloc(1, flen + 32)) == nullptr) {
fp.close();
return errNotEnoughMemory;
}
fp.read(mem, flen);
fp.close();
decodeObjects(mem, flen);
free(mem);