[Scummvm-git-logs] scummvm master -> bd2918d641fc24a5ef9b1235f11d086b322b22b7
sluicebox
noreply at scummvm.org
Thu Jan 23 11:18:19 UTC 2025
This automated email contains information about 4 new commits which have been
pushed to the 'scummvm' repo located at https://github.com/scummvm/scummvm .
Summary:
be53239eef AGI: Move AgiLoader definitions to loader.h
0ffed94e13 AGI: Create AgiLoader method for locating disk images
980dfe72d6 AGI: Add KQ1-Early PC loader and detection
bd2918d641 AGI: Add KQ1-Early Apple II loader and detection
Commit: be53239eefd2420dc676b9e9dfad6958f6792cee
https://github.com/scummvm/scummvm/commit/be53239eefd2420dc676b9e9dfad6958f6792cee
Author: sluicebox (22204938+sluicebox at users.noreply.github.com)
Date: 2025-01-23T03:18:14-08:00
Commit Message:
AGI: Move AgiLoader definitions to loader.h
Changed paths:
A engines/agi/loader.h
engines/agi/agi.cpp
engines/agi/agi.h
engines/agi/console.cpp
engines/agi/loader_a2.cpp
engines/agi/loader_v1.cpp
engines/agi/loader_v2.cpp
engines/agi/loader_v3.cpp
diff --git a/engines/agi/agi.cpp b/engines/agi/agi.cpp
index 17cc4ccbdc9..746b2b8bc2c 100644
--- a/engines/agi/agi.cpp
+++ b/engines/agi/agi.cpp
@@ -42,6 +42,7 @@
#include "agi/font.h"
#include "agi/graphics.h"
#include "agi/inv.h"
+#include "agi/loader.h"
#include "agi/sprite.h"
#include "agi/text.h"
#include "agi/keyboard.h"
diff --git a/engines/agi/agi.h b/engines/agi/agi.h
index 5e4a7dc68a8..cbfe622a5b4 100644
--- a/engines/agi/agi.h
+++ b/engines/agi/agi.h
@@ -541,149 +541,7 @@ struct AgiGame {
}
};
-struct AgiDiskVolume {
- uint32 disk;
- uint32 offset;
-
- AgiDiskVolume() : disk(_EMPTY), offset(0) {}
- AgiDiskVolume(uint32 d, uint32 o) : disk(d), offset(o) {}
-};
-
-/**
- * Apple II version of the format for LOGDIR, VIEWDIR, etc.
- * See AgiLoader_A2::loadDir for more details.
- */
-enum A2DirVersion {
- A2DirVersionOld, // 4 bits for volume, 8 for track
- A2DirVersionNew, // 5 bits for volume, 7 for track
-};
-
-class AgiLoader {
-public:
- AgiLoader(AgiEngine *vm) : _vm(vm) {}
- virtual ~AgiLoader() {}
-
- /**
- * Performs one-time initializations, such as locating files
- * with dynamic names.
- */
- virtual void init() {}
-
- /**
- * Loads all AGI directory entries from disk and and populates
- * the AgiDir arrays in AgiGame with them.
- */
- virtual int loadDirs() = 0;
-
- /**
- * Loads a volume resource from disk.
- */
- virtual uint8 *loadVolumeResource(AgiDir *agid) = 0;
-
- /**
- * Loads AgiEngine::_objects from disk.
- */
- virtual int loadObjects() = 0;
-
- /**
- * Loads AgiBase::_words from disk.
- */
- virtual int loadWords() = 0;
-
-protected:
- AgiEngine *_vm;
-};
-
-class AgiLoader_A2 : public AgiLoader {
-public:
- AgiLoader_A2(AgiEngine *vm) : AgiLoader(vm) {}
- ~AgiLoader_A2() override;
-
- void init() override;
- int loadDirs() override;
- uint8 *loadVolumeResource(AgiDir *agid) override;
- int loadObjects() override;
- int loadWords() override;
-
-private:
- Common::Array<Common::SeekableReadStream *> _disks;
- Common::Array<AgiDiskVolume> _volumes;
- AgiDir _logDir;
- AgiDir _picDir;
- AgiDir _viewDir;
- AgiDir _soundDir;
- AgiDir _objects;
- AgiDir _words;
-
- int readDiskOne(Common::SeekableReadStream &stream, Common::Array<uint32> &volumeMap);
- static bool readInitDir(Common::SeekableReadStream &stream, byte index, AgiDir &agid);
- static bool readDir(Common::SeekableReadStream &stream, int position, AgiDir &agid);
- static bool readVolumeMap(Common::SeekableReadStream &stream, uint32 position, uint32 bufferLength, Common::Array<uint32> &volumeMap);
-
- A2DirVersion detectDirVersion(Common::SeekableReadStream &stream);
- bool loadDir(AgiDir *dir, Common::SeekableReadStream &disk, uint32 dirOffset, uint32 dirLength, A2DirVersion dirVersion);
-};
-
-class AgiLoader_v1 : public AgiLoader {
-public:
- AgiLoader_v1(AgiEngine *vm) : AgiLoader(vm) {}
-
- void init() override;
- int loadDirs() override;
- uint8 *loadVolumeResource(AgiDir *agid) override;
- int loadObjects() override;
- int loadWords() override;
-
-private:
- Common::Array<Common::String> _imageFiles;
- Common::Array<AgiDiskVolume> _volumes;
- AgiDir _logDir;
- AgiDir _picDir;
- AgiDir _viewDir;
- AgiDir _soundDir;
- AgiDir _objects;
- AgiDir _words;
-
- bool readDiskOneV1(Common::SeekableReadStream &stream);
- bool readDiskOneV2001(Common::SeekableReadStream &stream, int &vol0Offset);
- static bool readInitDirV1(Common::SeekableReadStream &stream, byte index, AgiDir &agid);
- static bool readInitDirV2001(Common::SeekableReadStream &stream, byte index, AgiDir &agid);
-
- bool loadDir(AgiDir *dir, Common::File &disk, uint32 dirOffset, uint32 dirLength);
-};
-
-class AgiLoader_v2 : public AgiLoader {
-private:
- bool _hasV3VolumeFormat;
-
- int loadDir(AgiDir *agid, const char *fname);
- bool detectV3VolumeFormat();
-
-public:
- AgiLoader_v2(AgiEngine *vm) : _hasV3VolumeFormat(false), AgiLoader(vm) {}
-
- int loadDirs() override;
- uint8 *loadVolumeResource(AgiDir *agid) override;
- int loadObjects() override;
- int loadWords() override;
-};
-
-class AgiLoader_v3 : public AgiLoader {
-private:
- Common::String _name; /**< prefix in directory and/or volume file names (`GR' for goldrush) */
-
- int loadDir(AgiDir *agid, Common::File *fp, uint32 offs, uint32 len);
-
-public:
- AgiLoader_v3(AgiEngine *vm) : AgiLoader(vm) {}
-
- void init() override;
- int loadDirs() override;
- uint8 *loadVolumeResource(AgiDir *agid) override;
- int loadObjects() override;
- int loadWords() override;
-};
-
+class AgiLoader;
class GfxFont;
class GfxMgr;
class SpritesMgr;
diff --git a/engines/agi/console.cpp b/engines/agi/console.cpp
index 24919316401..ec18fb447c6 100644
--- a/engines/agi/console.cpp
+++ b/engines/agi/console.cpp
@@ -22,6 +22,7 @@
#include "agi/agi.h"
#include "agi/opcodes.h"
#include "agi/graphics.h"
+#include "agi/loader.h"
#include "agi/preagi/preagi.h"
#include "agi/preagi/mickey.h"
diff --git a/engines/agi/loader.h b/engines/agi/loader.h
new file mode 100644
index 00000000000..2f5bebbbabd
--- /dev/null
+++ b/engines/agi/loader.h
@@ -0,0 +1,172 @@
+/* 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/>.
+ *
+ */
+
+#ifndef AGI_LOADER_H
+#define AGI_LOADER_H
+
+namespace Agi {
+
+class AgiLoader {
+public:
+ AgiLoader(AgiEngine *vm) : _vm(vm) {}
+ virtual ~AgiLoader() {}
+
+ /**
+ * Performs one-time initializations, such as locating files
+ * with dynamic names.
+ */
+ virtual void init() {}
+
+ /**
+ * Loads all AGI directory entries from disk and and populates
+ * the AgiDir arrays in AgiGame with them.
+ */
+ virtual int loadDirs() = 0;
+
+ /**
+ * Loads a volume resource from disk.
+ */
+ virtual uint8 *loadVolumeResource(AgiDir *agid) = 0;
+
+ /**
+ * Loads AgiEngine::_objects from disk.
+ */
+ virtual int loadObjects() = 0;
+
+ /**
+ * Loads AgiBase::_words from disk.
+ */
+ virtual int loadWords() = 0;
+
+protected:
+ AgiEngine *_vm;
+};
+
+struct AgiDiskVolume {
+ uint32 disk;
+ uint32 offset;
+
+ AgiDiskVolume() : disk(_EMPTY), offset(0) {}
+ AgiDiskVolume(uint32 d, uint32 o) : disk(d), offset(o) {}
+};
+
+/**
+ * Apple II version of the format for LOGDIR, VIEWDIR, etc.
+ * See AgiLoader_A2::loadDir for more details.
+ */
+enum A2DirVersion {
+ A2DirVersionOld, // 4 bits for volume, 8 for track
+ A2DirVersionNew, // 5 bits for volume, 7 for track
+};
+
+class AgiLoader_A2 : public AgiLoader {
+public:
+ AgiLoader_A2(AgiEngine *vm) : AgiLoader(vm) {}
+ ~AgiLoader_A2() override;
+
+ void init() override;
+ int loadDirs() override;
+ uint8 *loadVolumeResource(AgiDir *agid) override;
+ int loadObjects() override;
+ int loadWords() override;
+
+private:
+ Common::Array<Common::SeekableReadStream *> _disks;
+ Common::Array<AgiDiskVolume> _volumes;
+ AgiDir _logDir;
+ AgiDir _picDir;
+ AgiDir _viewDir;
+ AgiDir _soundDir;
+ AgiDir _objects;
+ AgiDir _words;
+
+ int readDiskOne(Common::SeekableReadStream &stream, Common::Array<uint32> &volumeMap);
+ static bool readInitDir(Common::SeekableReadStream &stream, byte index, AgiDir &agid);
+ static bool readDir(Common::SeekableReadStream &stream, int position, AgiDir &agid);
+ static bool readVolumeMap(Common::SeekableReadStream &stream, uint32 position, uint32 bufferLength, Common::Array<uint32> &volumeMap);
+
+ A2DirVersion detectDirVersion(Common::SeekableReadStream &stream) const;
+ static bool loadDir(AgiDir *dir, Common::SeekableReadStream &disk, uint32 dirOffset, uint32 dirLength, A2DirVersion dirVersion);
+};
+
+class AgiLoader_v1 : public AgiLoader {
+public:
+ AgiLoader_v1(AgiEngine *vm) : AgiLoader(vm) {}
+
+ void init() override;
+ int loadDirs() override;
+ uint8 *loadVolumeResource(AgiDir *agid) override;
+ int loadObjects() override;
+ int loadWords() override;
+
+private:
+ Common::Array<Common::String> _imageFiles;
+ Common::Array<AgiDiskVolume> _volumes;
+ AgiDir _logDir;
+ AgiDir _picDir;
+ AgiDir _viewDir;
+ AgiDir _soundDir;
+ AgiDir _objects;
+ AgiDir _words;
+
+ bool readDiskOneV1(Common::SeekableReadStream &stream);
+ bool readDiskOneV2001(Common::SeekableReadStream &stream, int &vol0Offset);
+ static bool readInitDirV1(Common::SeekableReadStream &stream, byte index, AgiDir &agid);
+ static bool readInitDirV2001(Common::SeekableReadStream &stream, byte index, AgiDir &agid);
+
+ bool loadDir(AgiDir *dir, Common::File &disk, uint32 dirOffset, uint32 dirLength);
+};
+
+class AgiLoader_v2 : public AgiLoader {
+private:
+ bool _hasV3VolumeFormat;
+
+ int loadDir(AgiDir *agid, const char *fname);
+ bool detectV3VolumeFormat();
+
+public:
+ AgiLoader_v2(AgiEngine *vm) : _hasV3VolumeFormat(false), AgiLoader(vm) {}
+
+ int loadDirs() override;
+ uint8 *loadVolumeResource(AgiDir *agid) override;
+ int loadObjects() override;
+ int loadWords() override;
+};
+
+class AgiLoader_v3 : public AgiLoader {
+private:
+ Common::String _name; /**< prefix in directory and/or volume file names (`GR' for goldrush) */
+
+ int loadDir(AgiDir *agid, Common::File *fp, uint32 offs, uint32 len);
+
+public:
+ AgiLoader_v3(AgiEngine *vm) : AgiLoader(vm) {}
+
+ void init() override;
+ int loadDirs() override;
+ uint8 *loadVolumeResource(AgiDir *agid) override;
+ int loadObjects() override;
+ int loadWords() override;
+};
+
+} // End of namespace Agi
+
+#endif /* AGI_LOADER_H */
diff --git a/engines/agi/loader_a2.cpp b/engines/agi/loader_a2.cpp
index 9fb0fe5daeb..92653e7817f 100644
--- a/engines/agi/loader_a2.cpp
+++ b/engines/agi/loader_a2.cpp
@@ -21,6 +21,7 @@
#include "agi/agi.h"
#include "agi/disk_image.h"
+#include "agi/loader.h"
#include "agi/words.h"
#include "common/config-manager.h"
@@ -354,7 +355,7 @@ int AgiLoader_A2::loadDirs() {
return success ? errOK : errBadResource;
}
-A2DirVersion AgiLoader_A2::detectDirVersion(Common::SeekableReadStream &stream) {
+A2DirVersion AgiLoader_A2::detectDirVersion(Common::SeekableReadStream &stream) const {
// A2 DIR format:
// old new
// volume 4 bits 5 bits
@@ -366,7 +367,7 @@ A2DirVersion AgiLoader_A2::detectDirVersion(Common::SeekableReadStream &stream)
// It must exist in the new format, but can't exist in the old.
// In the new format it's the first resource in volume 1.
// In the old format it would be track 128, which is invalid.
- AgiDir *dirs[4] = { &_logDir, &_picDir, &_viewDir, &_soundDir };
+ const AgiDir *dirs[4] = { &_logDir, &_picDir, &_viewDir, &_soundDir };
for (int d = 0; d < 4; d++) {
stream.seek(dirs[d]->offset);
uint16 dirEntryCount = MIN<uint32>(dirs[d]->len / 3, MAX_DIRECTORY_ENTRIES);
diff --git a/engines/agi/loader_v1.cpp b/engines/agi/loader_v1.cpp
index 8291ac5ab8b..dbb653964b0 100644
--- a/engines/agi/loader_v1.cpp
+++ b/engines/agi/loader_v1.cpp
@@ -21,6 +21,7 @@
#include "agi/agi.h"
#include "agi/disk_image.h"
+#include "agi/loader.h"
#include "agi/words.h"
#include "common/config-manager.h"
diff --git a/engines/agi/loader_v2.cpp b/engines/agi/loader_v2.cpp
index cf686c04822..4cb46567ba3 100644
--- a/engines/agi/loader_v2.cpp
+++ b/engines/agi/loader_v2.cpp
@@ -22,6 +22,7 @@
#include "common/textconsole.h"
#include "agi/agi.h"
+#include "agi/loader.h"
#include "agi/lzw.h"
#include "agi/words.h"
diff --git a/engines/agi/loader_v3.cpp b/engines/agi/loader_v3.cpp
index db920bc2e4f..97530330306 100644
--- a/engines/agi/loader_v3.cpp
+++ b/engines/agi/loader_v3.cpp
@@ -20,6 +20,7 @@
*/
#include "agi/agi.h"
+#include "agi/loader.h"
#include "agi/lzw.h"
#include "agi/words.h"
Commit: 0ffed94e13e554a5edae99bc90bc2b075f40106d
https://github.com/scummvm/scummvm/commit/0ffed94e13e554a5edae99bc90bc2b075f40106d
Author: sluicebox (22204938+sluicebox at users.noreply.github.com)
Date: 2025-01-23T03:18:14-08:00
Commit Message:
AGI: Create AgiLoader method for locating disk images
Changed paths:
A engines/agi/loader.cpp
engines/agi/loader.h
engines/agi/loader_a2.cpp
engines/agi/loader_v1.cpp
engines/agi/module.mk
diff --git a/engines/agi/loader.cpp b/engines/agi/loader.cpp
new file mode 100644
index 00000000000..0287b2e08ce
--- /dev/null
+++ b/engines/agi/loader.cpp
@@ -0,0 +1,62 @@
+/* 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/loader.h"
+
+#include "common/config-manager.h"
+#include "common/fs.h"
+
+namespace Agi {
+
+void AgiLoader::getPotentialDiskImages(
+ const char * const *imageExtensions,
+ size_t imageExtensionCount,
+ Common::Array<Common::Path> &imageFiles,
+ FileMap &fileMap) {
+
+ // get all files in game directory
+ Common::FSList allFiles;
+ Common::FSNode dir(ConfMan.getPath("path"));
+ if (!dir.getChildren(allFiles, Common::FSNode::kListFilesOnly)) {
+ warning("invalid game path: %s", dir.getPath().toString(Common::Path::kNativeSeparator).c_str());
+ return;
+ }
+
+ // build array of files with provided disk image extensions
+ for (const Common::FSNode &file : allFiles) {
+ for (size_t i = 0; i < imageExtensionCount; i++) {
+ if (file.getName().hasSuffixIgnoreCase(imageExtensions[i])) {
+ Common::Path path = file.getPath();
+ imageFiles.push_back(path);
+ fileMap[path] = file;
+ break;
+ }
+ }
+ }
+
+ // sort potential image files by name.
+ // this is an important step for consistent results,
+ // and because the first disk is likely to be first.
+ Common::sort(imageFiles.begin(), imageFiles.end());
+}
+
+} // End of namespace Agi
diff --git a/engines/agi/loader.h b/engines/agi/loader.h
index 2f5bebbbabd..afd23471e26 100644
--- a/engines/agi/loader.h
+++ b/engines/agi/loader.h
@@ -58,6 +58,13 @@ public:
protected:
AgiEngine *_vm;
+
+ typedef Common::HashMap<Common::Path, Common::FSNode, Common::Path::IgnoreCase_Hash, Common::Path::IgnoreCase_EqualTo> FileMap;
+ static void getPotentialDiskImages(
+ const char * const *imageExtensions,
+ size_t imageExtensionCount,
+ Common::Array<Common::Path> &imageFiles,
+ FileMap &fileMap);
};
struct AgiDiskVolume {
diff --git a/engines/agi/loader_a2.cpp b/engines/agi/loader_a2.cpp
index 92653e7817f..3e52ffa198c 100644
--- a/engines/agi/loader_a2.cpp
+++ b/engines/agi/loader_a2.cpp
@@ -24,7 +24,6 @@
#include "agi/loader.h"
#include "agi/words.h"
-#include "common/config-manager.h"
#include "common/formats/disk_image.h"
#include "common/fs.h"
#include "common/memstream.h"
@@ -59,8 +58,6 @@ namespace Agi {
// AgiMetaEngineDetection also scans for usable disk images. It finds the LOGDIR
// file inside disk one, hashes LOGDIR, and matches against the detection table.
-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];
@@ -68,30 +65,10 @@ AgiLoader_A2::~AgiLoader_A2() {
}
void AgiLoader_A2::init() {
- // get all files in game directory
- Common::FSList allFiles;
- Common::FSNode dir(ConfMan.getPath("path"));
- if (!dir.getChildren(allFiles, Common::FSNode::kListFilesOnly)) {
- warning("AgiLoader_A2: invalid game path: %s", dir.getPath().toString(Common::Path::kNativeSeparator).c_str());
- return;
- }
-
- // build array of files with a2 disk image extensions
+ // build sorted array of files with image extensions
Common::Array<Common::Path> imageFiles;
FileMap fileMap;
- for (const Common::FSNode &file : allFiles) {
- for (int i = 0; i < ARRAYSIZE(a2DiskImageExtensions); i++) {
- if (file.getName().hasSuffixIgnoreCase(a2DiskImageExtensions[i])) {
- Common::Path path = file.getPath();
- imageFiles.push_back(path);
- fileMap[path] = file;
- break;
- }
- }
- }
-
- // sort potential image files by name
- Common::sort(imageFiles.begin(), imageFiles.end());
+ getPotentialDiskImages(a2DiskImageExtensions, ARRAYSIZE(a2DiskImageExtensions), imageFiles, fileMap);
// find disk one by reading potential images until successful
int diskCount = 0;
diff --git a/engines/agi/loader_v1.cpp b/engines/agi/loader_v1.cpp
index dbb653964b0..36e8be6c07f 100644
--- a/engines/agi/loader_v1.cpp
+++ b/engines/agi/loader_v1.cpp
@@ -24,7 +24,6 @@
#include "agi/loader.h"
#include "agi/words.h"
-#include "common/config-manager.h"
#include "common/fs.h"
namespace Agi {
@@ -50,32 +49,11 @@ namespace Agi {
// AgiMetaEngineDetection also scans for usable disk images. It finds the LOGDIR
// file inside disk one, hashes LOGDIR, and matches against the detection table.
-typedef Common::HashMap<Common::Path, Common::FSNode, Common::Path::IgnoreCase_Hash, Common::Path::IgnoreCase_EqualTo> FileMap;
-
void AgiLoader_v1::init() {
- // get all files in game directory
- Common::FSList allFiles;
- Common::FSNode dir(ConfMan.getPath("path"));
- if (!dir.getChildren(allFiles, Common::FSNode::kListFilesOnly)) {
- warning("AgiLoader_v1: invalid game path: %s", dir.getPath().toString(Common::Path::kNativeSeparator).c_str());
- return;
- }
-
- // build array of files with pc disk image extensions
+ // build sorted array of files with image extensions
Common::Array<Common::Path> imageFiles;
FileMap fileMap;
- for (const Common::FSNode &file : allFiles) {
- for (int i = 0; i < ARRAYSIZE(pcDiskImageExtensions); i++) {
- if (file.getName().hasSuffixIgnoreCase(pcDiskImageExtensions[i])) {
- Common::Path path = file.getPath();
- imageFiles.push_back(path);
- fileMap[path] = file;
- }
- }
- }
-
- // sort potential image files by name
- Common::sort(imageFiles.begin(), imageFiles.end());
+ getPotentialDiskImages(pcDiskImageExtensions, ARRAYSIZE(pcDiskImageExtensions), imageFiles, fileMap);
// find disk one by reading potential images until successful
uint diskOneIndex;
diff --git a/engines/agi/module.mk b/engines/agi/module.mk
index b79da837f3c..5bb986a11c8 100644
--- a/engines/agi/module.mk
+++ b/engines/agi/module.mk
@@ -11,6 +11,7 @@ MODULE_OBJS := \
graphics.o \
inv.o \
keyboard.o \
+ loader.o \
loader_a2.o \
loader_v1.o \
loader_v2.o \
Commit: 980dfe72d6d217b34cbb27e761dae07c3b2808d0
https://github.com/scummvm/scummvm/commit/980dfe72d6d217b34cbb27e761dae07c3b2808d0
Author: sluicebox (22204938+sluicebox at users.noreply.github.com)
Date: 2025-01-23T03:18:14-08:00
Commit Message:
AGI: Add KQ1-Early PC loader and detection
Changed paths:
A engines/agi/loader_gal.cpp
engines/agi/agi.cpp
engines/agi/agi.h
engines/agi/detection.cpp
engines/agi/detection_tables.h
engines/agi/disk_image.h
engines/agi/loader.h
engines/agi/metaengine.cpp
engines/agi/module.mk
diff --git a/engines/agi/agi.cpp b/engines/agi/agi.cpp
index 746b2b8bc2c..ad8bef1bea2 100644
--- a/engines/agi/agi.cpp
+++ b/engines/agi/agi.cpp
@@ -169,6 +169,13 @@ int AgiEngine::agiInit() {
applyVolumeToMixer();
+ // Error on Game Adaptation Language, because it is not implemented yet.
+ // This allows testing the GAL components that have been developed, such
+ // as the resource loader, with our debug console.
+ if (getGameType() == GType_GAL) {
+ error("Game Adaptation Language not implemented yet");
+ }
+
return ec;
}
@@ -521,7 +528,9 @@ void AgiEngine::initialize() {
_text->charAttrib_Set(15, 0);
- if (getPlatform() == Common::kPlatformApple2) {
+ if (getGameType() == GType_GAL) {
+ _loader = new GalLoader(this);
+ } else if (getPlatform() == Common::kPlatformApple2) {
_loader = new AgiLoader_A2(this);
} else if (getVersion() <= 0x2001) {
_loader = new AgiLoader_v1(this);
diff --git a/engines/agi/agi.h b/engines/agi/agi.h
index cbfe622a5b4..a46f38d18d0 100644
--- a/engines/agi/agi.h
+++ b/engines/agi/agi.h
@@ -99,10 +99,11 @@ namespace Agi {
enum AgiGameType {
GType_PreAGI = 0,
- GType_V1 = 1,
- GType_V2 = 2,
- GType_V3 = 3,
- GType_A2 = 4
+ GType_V1 = 1,
+ GType_V2 = 2,
+ GType_V3 = 3,
+ GType_A2 = 4,
+ GType_GAL = 5
};
enum AgiGameFeatures {
diff --git a/engines/agi/detection.cpp b/engines/agi/detection.cpp
index 6999c0accc3..865b653004f 100644
--- a/engines/agi/detection.cpp
+++ b/engines/agi/detection.cpp
@@ -19,7 +19,6 @@
*
*/
-
#include "common/config-manager.h"
#include "common/system.h"
#include "common/debug.h"
@@ -129,6 +128,8 @@ private:
static Common::String getLogDirHashFromA2DiskImage(Common::SeekableReadStream &stream);
static Common::String getLogDirHashFromDiskImage(Common::SeekableReadStream &stream, uint32 position);
+
+ static Common::String getGalDirHashFromDiskImage(Common::SeekableReadStream &stream);
};
ADDetectedGame AgiMetaEngineDetection::fallbackDetect(const FileMap &allFilesXXX, const Common::FSList &fslist, ADDetectedGameExtraInfo **extra) const {
@@ -359,9 +360,10 @@ void AgiMetaEngineDetection::getPotentialDiskImages(
}
/**
- * Detects a PC Booter game by searching for 360k floppy images, reading LOGDIR,
- * hashing LOGDIR, and comparing to DOS GType_V1 entries in the detection table.
- * See AgiLoader_v1 in loader_v1.cpp for more details.
+ * Detects a PC Booter game by searching for 360k floppy images, reading LOGDIR
+ * or GAL's directory, hashing, and comparing to DOS GType_V1 and GType_GAL
+ * entries in the detection table.
+ * See AgiLoader_v1 and GalLoader for more details.
*/
ADDetectedGame AgiMetaEngineDetection::detectPcDiskImageGame(const FileMap &allFiles, uint32 skipADFlags) {
// build array of files with pc disk image extensions
@@ -378,6 +380,8 @@ ADDetectedGame AgiMetaEngineDetection::detectPcDiskImageGame(const FileMap &allF
// attempt to locate and hash logdir using both possible inidir disk locations
Common::String logdirHash1 = getLogDirHashFromPcDiskImageV1(*stream);
Common::String logdirHash2 = getLogDirHashFromPcDiskImageV2001(*stream);
+ // attempt to locate and hash GAL directory
+ Common::String galDirHash = getGalDirHashFromDiskImage(*stream);
delete stream;
if (!logdirHash1.empty()) {
@@ -386,19 +390,28 @@ ADDetectedGame AgiMetaEngineDetection::detectPcDiskImageGame(const FileMap &allF
if (!logdirHash2.empty()) {
debug(3, "pc disk logdir hash: %s, %s", logdirHash2.c_str(), imageFile.baseName().c_str());
}
+ if (!galDirHash.empty()) {
+ debug(3, "pc disk gal dir hash: %s, %s", galDirHash.c_str(), imageFile.baseName().c_str());
+ }
- // if logdir hash found then compare against hashes of DOS GType_V1 entries
- if (!logdirHash1.empty() || !logdirHash2.empty()) {
+ // if hash found then compare against hashes of DOS GType_V1 entries
+ if (!logdirHash1.empty() || !logdirHash2.empty() || !galDirHash.empty()) {
for (const AGIGameDescription *game = gameDescriptions; game->desc.gameId != nullptr; game++) {
- if (game->desc.platform == Common::kPlatformDOS && game->gameType == GType_V1 && !(game->desc.flags & skipADFlags)) {
+ if (game->desc.platform == Common::kPlatformDOS &&
+ (game->gameType == GType_V1 || game->gameType == GType_GAL) &&
+ !(game->desc.flags & skipADFlags)) {
+
const ADGameFileDescription *file;
for (file = game->desc.filesDescriptions; file->fileName != nullptr; file++) {
- // select the logdir hash to use by the game's interpreter version
- Common::String &logdirHash = (game->version < 0x2001) ? logdirHash1 : logdirHash2;
- if (file->md5 != nullptr && !logdirHash.empty() && file->md5 == logdirHash) {
+ // select the hash hash to use
+ Common::String &hash = (game->gameType == GType_V1) ?
+ ((game->version < 0x2001) ? logdirHash1 : logdirHash2) :
+ galDirHash;
+
+ if (file->md5 != nullptr && !hash.empty() && file->md5 == hash) {
debug(3, "disk image match: %s, %s, %s", game->desc.gameId, game->desc.extra, imageFile.baseName().c_str());
- // logdir hash match found
+ // hash match found
ADDetectedGame detectedGame(&game->desc);
FileProperties fileProps;
fileProps.md5 = file->md5;
@@ -579,6 +592,44 @@ Common::String AgiMetaEngineDetection::getLogDirHashFromDiskImage(Common::Seekab
return Common::computeStreamMD5AsString(stream, logDirSize);
}
+Common::String AgiMetaEngineDetection::getGalDirHashFromDiskImage(Common::SeekableReadStream &stream) {
+ static const uint16 dirPositions[] = { GAL_DIR_POSITION_PCJR, GAL_DIR_POSITION_PC };
+ for (int i = 0; i < 2; i++) {
+ stream.seek(dirPositions[i]);
+
+ // read logic 0 position
+ 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;
+ uint32 logicPosition = (sector * 512) + offset;
+
+ // read logic 0 header, calculate length
+ stream.seek(logicPosition);
+ uint32 logicSize = 8;
+ for (int j = 0; j < 4; j++) {
+ logicSize += stream.readUint16LE();
+ }
+ if (stream.eos()) {
+ continue;
+ }
+
+ // confirm that logic ends in terminator
+ stream.seek(logicPosition + logicSize -1);
+ byte logicTerminator = stream.readByte();
+ if (stream.eos() || logicTerminator != 0xff) {
+ continue;
+ }
+
+ // hash the directory
+ stream.seek(dirPositions[i]);
+ return Common::computeStreamMD5AsString(stream, GAL_DIR_SIZE);
+ }
+ return "";
+}
+
} // end of namespace Agi
REGISTER_PLUGIN_STATIC(AGI_DETECTION, PLUGIN_TYPE_ENGINE_DETECTION, Agi::AgiMetaEngineDetection);
diff --git a/engines/agi/detection_tables.h b/engines/agi/detection_tables.h
index 520db710649..d033f28ccf8 100644
--- a/engines/agi/detection_tables.h
+++ b/engines/agi/detection_tables.h
@@ -117,7 +117,7 @@ namespace Agi {
#define A2_CP(id,extra,md5,ver,gid) GAME_LVFPN_FLAGS(id,extra,"*",md5,AD_NO_SIZE,Common::EN_ANY,ver,0,gid,Common::kPlatformApple2,GType_A2,GAMEOPTIONS_DEFAULT_CP,ADGF_UNSTABLE)
#define BOOTER(id,extra,md5,ver,gid) GAME_LVFPN(id,extra,"*",md5,AD_NO_SIZE,Common::EN_ANY,ver,0,gid,Common::kPlatformDOS,GType_V1,GAMEOPTIONS_DEFAULT)
#define BOOTER_UNSTABLE(id,extra,md5,ver,gid) GAME_LVFPN_FLAGS(id,extra,"*",md5,AD_NO_SIZE,Common::EN_ANY,ver,0,gid,Common::kPlatformDOS,GType_V1,GAMEOPTIONS_DEFAULT,ADGF_UNSTABLE)
-#define BOOTER_UNSUPPORTED(id,msg,fname,md5,size,ver,gid) GAME_LVFPN_FLAGS(id,msg,fname,md5,size,Common::EN_ANY,ver,0,gid,Common::kPlatformDOS,GType_V1,GAMEOPTIONS_DEFAULT,ADGF_UNSUPPORTED)
+#define BOOTER_GAL(id,extra,md5,ver,gid) GAME_LVFPN_FLAGS(id,extra,"*",md5,AD_NO_SIZE,Common::EN_ANY,ver,0,gid,Common::kPlatformDOS,GType_GAL,GAMEOPTIONS_DEFAULT,ADGF_UNSUPPORTED)
#define GAME(id,extra,md5,ver,gid) GAME_LVFPN(id,extra,"logdir",md5,AD_NO_SIZE,Common::EN_ANY,ver,0,gid,Common::kPlatformDOS,GType_V2,GAMEOPTIONS_DEFAULT)
#define GAME3(id,extra,fname,md5,ver,gid) GAME_LVFPN(id,extra,fname,md5,AD_NO_SIZE,Common::EN_ANY,ver,0,gid,Common::kPlatformDOS,GType_V3,GAMEOPTIONS_DEFAULT)
#define GAME3_CP(id,extra,fname,md5,ver,gid) GAME_LVFPN(id,extra,fname,md5,AD_NO_SIZE,Common::EN_ANY,ver,0,gid,Common::kPlatformDOS,GType_V3,GAMEOPTIONS_DEFAULT_CP)
@@ -343,13 +343,19 @@ static const AGIGameDescription gameDescriptions[] = {
// King's Quest 1 (Mac) 2.0C 3/26/87
GAME_P("kq1", "2.0C 1987-03-26", "d4c4739d4ac63f7dbd29255425077d48", 0x2440, GID_KQ1, Common::kPlatformMacintosh),
- // King's Quest 1 (IBM PCjr) 1.00 1502265 5/10/84
- BOOTER_UNSUPPORTED("kq1", "Early King\'s Quest releases are not currently supported.",
- "kq1.img", "127675735f9d2c148738c1e96ea9d2cf", 368640, 0x1120, GID_KQ1),
+ // King's Quest 1 (IBM PCjr) 1984-05-10
+ BOOTER_GAL("kq1", "Early King\'s Quest releases are not currently supported.", "0d1cca805d08438a1dc83431b7348fe3", 0x1000, GID_KQ1),
- // King's Quest 1 (Tandy 1000) 01.01.00 5/24/84
- BOOTER_UNSUPPORTED("kq1", "Early King\'s Quest releases are not currently supported.",
- "kq1.img", "0a22131d0eaf66d955afecfdc83ef9d6", 368640, 0x1120, GID_KQ1),
+ // King's Quest 1 (IBM PC CGA) 1984-05-30
+ BOOTER_GAL("kq1", "Early King\'s Quest releases are not currently supported.", "6ba3a845502508c99a6cb2eed92f030d", 0x1000, GID_KQ1),
+
+ // King's Quest 1 (IBM PC CGA+RGB) 1984-08-16
+ BOOTER_GAL("kq1", "Early King\'s Quest releases are not currently supported.", "f44abc925bbfee1fede7ba42708a6d00", 0x1000, GID_KQ1),
+
+ // King's Quest 1 (Tandy 1000) 01.01.00 1985-05-24
+ // King's Quest 1 (Tandy 1000 + IBM PCjr) 1985-09-04
+ // These versions have identical resources and resource directories
+ BOOTER_GAL("kq1", "Early King\'s Quest releases are not currently supported.", "5be8342f00f7d951d0a4ee2e5c9f5b31", 0x1000, GID_KQ1),
// King's Quest 1 (PC 5.25"/3.5") 2.0F [AGI 2.917]
GAME("kq1", "2.0F 1987-05-05 5.25\"/3.5\"", "10ad66e2ecbd66951534a50aedcd0128", 0x2917, GID_KQ1),
diff --git a/engines/agi/disk_image.h b/engines/agi/disk_image.h
index 941c081d5cf..bb5a01a5e6b 100644
--- a/engines/agi/disk_image.h
+++ b/engines/agi/disk_image.h
@@ -98,6 +98,17 @@ static const char * const a2DiskImageExtensions[] = { ".do", ".dsk", ".img", ".n
#define A2_BC_DISK_COUNT 5
#define A2_BC_VOLUME_COUNT 9
+// GAL disk image values and helpers for GalLoader and AgiMetaEngineDetection
+
+#define GAL_LOGIC_COUNT 84
+#define GAL_PICTURE_COUNT 84
+#define GAL_VIEW_COUNT 110
+#define GAL_SOUND_COUNT 10
+
+#define GAL_DIR_POSITION_PCJR 0x0500
+#define GAL_DIR_POSITION_PC 0x1400
+#define GAL_DIR_SIZE 948
+
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);
diff --git a/engines/agi/loader.h b/engines/agi/loader.h
index afd23471e26..028a0047acc 100644
--- a/engines/agi/loader.h
+++ b/engines/agi/loader.h
@@ -174,6 +174,24 @@ public:
int loadWords() override;
};
+class GalLoader : public AgiLoader {
+public:
+ GalLoader(AgiEngine *vm) : AgiLoader(vm) {}
+
+ void init() override;
+ int loadDirs() override;
+ uint8 *loadVolumeResource(AgiDir *agid) override;
+ int loadObjects() override;
+ int loadWords() override;
+
+private:
+ Common::String _imageFile;
+ int _dirOffset;
+
+ static bool isDirectory(Common::SeekableReadStream &stream, uint32 dirOffset);
+ static uint32 readDirectoryEntry(Common::SeekableReadStream &stream, uint32 *sectorCount);
+};
+
} // End of namespace Agi
#endif /* AGI_LOADER_H */
diff --git a/engines/agi/loader_gal.cpp b/engines/agi/loader_gal.cpp
new file mode 100644
index 00000000000..62b24d74e1c
--- /dev/null
+++ b/engines/agi/loader_gal.cpp
@@ -0,0 +1,256 @@
+/* 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 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<Common::Path> 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
diff --git a/engines/agi/metaengine.cpp b/engines/agi/metaengine.cpp
index 4bf41a0b7b0..2d10bc543cf 100644
--- a/engines/agi/metaengine.cpp
+++ b/engines/agi/metaengine.cpp
@@ -259,6 +259,7 @@ Common::Error AgiMetaEngine::createInstance(OSystem *syst, Engine **engine, cons
case Agi::GType_V2:
case Agi::GType_V3:
case Agi::GType_A2:
+ case Agi::GType_GAL:
*engine = new Agi::AgiEngine(syst, gd);
break;
default:
diff --git a/engines/agi/module.mk b/engines/agi/module.mk
index 5bb986a11c8..b471c081eb8 100644
--- a/engines/agi/module.mk
+++ b/engines/agi/module.mk
@@ -13,6 +13,7 @@ MODULE_OBJS := \
keyboard.o \
loader.o \
loader_a2.o \
+ loader_gal.o \
loader_v1.o \
loader_v2.o \
loader_v3.o \
Commit: bd2918d641fc24a5ef9b1235f11d086b322b22b7
https://github.com/scummvm/scummvm/commit/bd2918d641fc24a5ef9b1235f11d086b322b22b7
Author: sluicebox (22204938+sluicebox at users.noreply.github.com)
Date: 2025-01-23T03:18:14-08:00
Commit Message:
AGI: Add KQ1-Early Apple II loader and detection
Changed paths:
A engines/agi/loader_gal_a2.cpp
engines/agi/agi.cpp
engines/agi/detection.cpp
engines/agi/detection_tables.h
engines/agi/disk_image.h
engines/agi/loader.h
engines/agi/module.mk
diff --git a/engines/agi/agi.cpp b/engines/agi/agi.cpp
index ad8bef1bea2..7280068fa63 100644
--- a/engines/agi/agi.cpp
+++ b/engines/agi/agi.cpp
@@ -529,7 +529,11 @@ void AgiEngine::initialize() {
_text->charAttrib_Set(15, 0);
if (getGameType() == GType_GAL) {
- _loader = new GalLoader(this);
+ if (getPlatform() == Common::kPlatformApple2) {
+ _loader = new GalLoader_A2(this);
+ } else {
+ _loader = new GalLoader(this);
+ }
} else if (getPlatform() == Common::kPlatformApple2) {
_loader = new AgiLoader_A2(this);
} else if (getVersion() <= 0x2001) {
diff --git a/engines/agi/detection.cpp b/engines/agi/detection.cpp
index 865b653004f..23b6ead8fba 100644
--- a/engines/agi/detection.cpp
+++ b/engines/agi/detection.cpp
@@ -129,7 +129,8 @@ private:
static Common::String getLogDirHashFromDiskImage(Common::SeekableReadStream &stream, uint32 position);
- static Common::String getGalDirHashFromDiskImage(Common::SeekableReadStream &stream);
+ static Common::String getGalDirHashFromPcDiskImage(Common::SeekableReadStream &stream);
+ static Common::String getGalDirHashFromA2DiskImage(Common::SeekableReadStream &stream);
};
ADDetectedGame AgiMetaEngineDetection::fallbackDetect(const FileMap &allFilesXXX, const Common::FSList &fslist, ADDetectedGameExtraInfo **extra) const {
@@ -380,8 +381,8 @@ ADDetectedGame AgiMetaEngineDetection::detectPcDiskImageGame(const FileMap &allF
// attempt to locate and hash logdir using both possible inidir disk locations
Common::String logdirHash1 = getLogDirHashFromPcDiskImageV1(*stream);
Common::String logdirHash2 = getLogDirHashFromPcDiskImageV2001(*stream);
- // attempt to locate and hash GAL directory
- Common::String galDirHash = getGalDirHashFromDiskImage(*stream);
+ // attempt to locate and hash GAL directory
+ Common::String galDirHash = getGalDirHashFromPcDiskImage(*stream);
delete stream;
if (!logdirHash1.empty()) {
@@ -505,10 +506,12 @@ ADDetectedGame AgiMetaEngineDetection::detectA2DiskImageGame(const FileMap &allF
Common::String logdirHashInitdir = getLogDirHashFromA2DiskImage(*stream);
Common::String logdirHashBc = getLogDirHashFromDiskImage(*stream, A2_BC_LOGDIR_POSITION);
Common::String logdirHashKq2 = getLogDirHashFromDiskImage(*stream, A2_KQ2_LOGDIR_POSITION);
+ // attempt to locate and hash GAL directory.
+ Common::String logdirHashKq1 = getGalDirHashFromA2DiskImage(*stream);
delete stream;
if (!logdirHashInitdir.empty()) {
- debug(3, "disk image logdir hash: %s, %s", logdirHashInitdir.c_str(), imageFile.baseName().c_str());
+ debug(3, "disk image initdir hash: %s, %s", logdirHashInitdir.c_str(), imageFile.baseName().c_str());
}
if (!logdirHashBc.empty()) {
debug(3, "disk image logdir hash: %s, %s", logdirHashBc.c_str(), imageFile.baseName().c_str());
@@ -518,7 +521,7 @@ ADDetectedGame AgiMetaEngineDetection::detectA2DiskImageGame(const FileMap &allF
}
// if logdir hash found then compare against hashes of Apple II entries
- if (!logdirHashInitdir.empty() || !logdirHashBc.empty() || !logdirHashKq2.empty()) {
+ if (!logdirHashInitdir.empty() || !logdirHashBc.empty() || !logdirHashKq2.empty() || !logdirHashKq1.empty()) {
for (const AGIGameDescription *game = gameDescriptions; game->desc.gameId != nullptr; game++) {
if (game->desc.platform == Common::kPlatformApple2 && !(game->desc.flags & skipADFlags)) {
const ADGameFileDescription *file;
@@ -526,6 +529,7 @@ ADDetectedGame AgiMetaEngineDetection::detectA2DiskImageGame(const FileMap &allF
// select the logdir hash to use
Common::String &logdirHash = (game->gameID == GID_BC) ? logdirHashBc :
(game->gameID == GID_KQ2) ? logdirHashKq2 :
+ (game->gameID == GID_KQ1) ? logdirHashKq1 :
logdirHashInitdir;
if (file->md5 != nullptr && !logdirHash.empty() && file->md5 == logdirHash) {
debug(3, "disk image match: %s, %s, %s", game->desc.gameId, game->desc.extra, imageFile.baseName().c_str());
@@ -592,7 +596,7 @@ Common::String AgiMetaEngineDetection::getLogDirHashFromDiskImage(Common::Seekab
return Common::computeStreamMD5AsString(stream, logDirSize);
}
-Common::String AgiMetaEngineDetection::getGalDirHashFromDiskImage(Common::SeekableReadStream &stream) {
+Common::String AgiMetaEngineDetection::getGalDirHashFromPcDiskImage(Common::SeekableReadStream &stream) {
static const uint16 dirPositions[] = { GAL_DIR_POSITION_PCJR, GAL_DIR_POSITION_PC };
for (int i = 0; i < 2; i++) {
stream.seek(dirPositions[i]);
@@ -630,6 +634,12 @@ Common::String AgiMetaEngineDetection::getGalDirHashFromDiskImage(Common::Seekab
return "";
}
+Common::String AgiMetaEngineDetection::getGalDirHashFromA2DiskImage(Common::SeekableReadStream &stream) {
+ // hash the directory
+ stream.seek(GAL_A2_LOGDIR_POSITION);
+ return Common::computeStreamMD5AsString(stream, GAL_A2_LOGDIR_SIZE);
+}
+
} // end of namespace Agi
REGISTER_PLUGIN_STATIC(AGI_DETECTION, PLUGIN_TYPE_ENGINE_DETECTION, Agi::AgiMetaEngineDetection);
diff --git a/engines/agi/detection_tables.h b/engines/agi/detection_tables.h
index d033f28ccf8..71e49c63733 100644
--- a/engines/agi/detection_tables.h
+++ b/engines/agi/detection_tables.h
@@ -115,6 +115,7 @@ namespace Agi {
#define A2(id,extra,md5,ver,gid) GAME_LVFPN_FLAGS(id,extra,"*",md5,AD_NO_SIZE,Common::EN_ANY,ver,0,gid,Common::kPlatformApple2,GType_A2,GAMEOPTIONS_DEFAULT,ADGF_UNSTABLE)
#define A2_CP(id,extra,md5,ver,gid) GAME_LVFPN_FLAGS(id,extra,"*",md5,AD_NO_SIZE,Common::EN_ANY,ver,0,gid,Common::kPlatformApple2,GType_A2,GAMEOPTIONS_DEFAULT_CP,ADGF_UNSTABLE)
+#define A2_GAL(id,extra,md5,ver,gid) GAME_LVFPN_FLAGS(id,extra,"*",md5,AD_NO_SIZE,Common::EN_ANY,ver,0,gid,Common::kPlatformApple2,GType_GAL,GAMEOPTIONS_DEFAULT,ADGF_UNSUPPORTED)
#define BOOTER(id,extra,md5,ver,gid) GAME_LVFPN(id,extra,"*",md5,AD_NO_SIZE,Common::EN_ANY,ver,0,gid,Common::kPlatformDOS,GType_V1,GAMEOPTIONS_DEFAULT)
#define BOOTER_UNSTABLE(id,extra,md5,ver,gid) GAME_LVFPN_FLAGS(id,extra,"*",md5,AD_NO_SIZE,Common::EN_ANY,ver,0,gid,Common::kPlatformDOS,GType_V1,GAMEOPTIONS_DEFAULT,ADGF_UNSTABLE)
#define BOOTER_GAL(id,extra,md5,ver,gid) GAME_LVFPN_FLAGS(id,extra,"*",md5,AD_NO_SIZE,Common::EN_ANY,ver,0,gid,Common::kPlatformDOS,GType_GAL,GAMEOPTIONS_DEFAULT,ADGF_UNSUPPORTED)
@@ -343,6 +344,9 @@ static const AGIGameDescription gameDescriptions[] = {
// King's Quest 1 (Mac) 2.0C 3/26/87
GAME_P("kq1", "2.0C 1987-03-26", "d4c4739d4ac63f7dbd29255425077d48", 0x2440, GID_KQ1, Common::kPlatformMacintosh),
+ // King's Quest 1 (Apple II)
+ A2_GAL("kq1", "Early King\'s Quest releases are not currently supported.", "a59f92b2d6e4fd245a8d51acdc58fc6d", 0x1000, GID_KQ1),
+
// King's Quest 1 (IBM PCjr) 1984-05-10
BOOTER_GAL("kq1", "Early King\'s Quest releases are not currently supported.", "0d1cca805d08438a1dc83431b7348fe3", 0x1000, GID_KQ1),
diff --git a/engines/agi/disk_image.h b/engines/agi/disk_image.h
index bb5a01a5e6b..741e4ec6f8b 100644
--- a/engines/agi/disk_image.h
+++ b/engines/agi/disk_image.h
@@ -109,6 +109,19 @@ static const char * const a2DiskImageExtensions[] = { ".do", ".dsk", ".img", ".n
#define GAL_DIR_POSITION_PC 0x1400
#define GAL_DIR_SIZE 948
+// GAL disk image values and helpers for GalLoader_A2 and AgiMetaEngineDetection
+
+#define GAL_A2_LOGIC_COUNT 81
+#define GAL_A2_PICTURE_COUNT 85
+#define GAL_A2_VIEW_COUNT 110
+
+#define GAL_A2_LOGDIR_POSITION A2_DISK_POSITION(18, 7, 2)
+#define GAL_A2_PICDIR_POSITION A2_DISK_POSITION(18, 6, 2)
+#define GAL_A2_VIEWDIR_POSITION A2_DISK_POSITION(18, 8, 2)
+#define GAL_A2_WORDS_POSITION A2_DISK_POSITION(17, 8, 0)
+#define GAL_A2_LOGDIR_SIZE (GAL_A2_LOGIC_COUNT * 3)
+#define GAL_A2_DISK_COUNT 3
+
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);
diff --git a/engines/agi/loader.h b/engines/agi/loader.h
index 028a0047acc..aca9406ef07 100644
--- a/engines/agi/loader.h
+++ b/engines/agi/loader.h
@@ -192,6 +192,27 @@ private:
static uint32 readDirectoryEntry(Common::SeekableReadStream &stream, uint32 *sectorCount);
};
+class GalLoader_A2 : public AgiLoader {
+public:
+ GalLoader_A2(AgiEngine *vm) : AgiLoader(vm) {}
+ ~GalLoader_A2();
+
+ void init() override;
+ int loadDirs() override;
+ uint8 *loadVolumeResource(AgiDir *agid) override;
+ int loadObjects() override;
+ int loadWords() override;
+
+private:
+ Common::Array<Common::SeekableReadStream *> _disks;
+
+ static bool readDiskOne(Common::SeekableReadStream &disk, AgiDir *logicDir);
+ static bool readDirectoryEntry(Common::SeekableReadStream &stream, AgiDir &dirEntry);
+ static bool validateDisk(Common::SeekableReadStream &disk, byte diskIndex, AgiDir *logicDir);
+
+ static bool loadDir(AgiDir *dir, Common::SeekableReadStream &disk, uint32 dirOffset, uint32 dirCount);
+};
+
} // End of namespace Agi
#endif /* AGI_LOADER_H */
diff --git a/engines/agi/loader_gal_a2.cpp b/engines/agi/loader_gal_a2.cpp
new file mode 100644
index 00000000000..c3cd32362b3
--- /dev/null
+++ b/engines/agi/loader_gal_a2.cpp
@@ -0,0 +1,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
diff --git a/engines/agi/module.mk b/engines/agi/module.mk
index b471c081eb8..34c16d86be6 100644
--- a/engines/agi/module.mk
+++ b/engines/agi/module.mk
@@ -14,6 +14,7 @@ MODULE_OBJS := \
loader.o \
loader_a2.o \
loader_gal.o \
+ loader_gal_a2.o \
loader_v1.o \
loader_v2.o \
loader_v3.o \
More information about the Scummvm-git-logs
mailing list