[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, &sectorCount);
+
+		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, &sectorCount);
+
+		// 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, &sectorCount);
+
+		// 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, &sectorCount);
+
+		// 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