[Scummvm-git-logs] scummvm master -> ff0d6321ec8d61e273742071ee5347ae74182dd4

sev- noreply at scummvm.org
Tue Aug 19 14:00:56 UTC 2025


This automated email contains information about 9 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .

Summary:
acc41ee325 DIRECTOR: Play sound channels stored in film loops
6e6a1dc05c DIRECTOR: XOBJ: Add stub for Pharaohs
7855a13b51 DIRECTOR: Improve accuracy of path resolution
523b2d63c9 DIRECTOR: LINGO: closeXlib() does not affect Xtras
63a6898b1a DIRECTOR: Add detection patch for GORD at K
6c3f823faf DIRECTOR: LINGO: Improve accuracy of b_min/b_max
111f38cb29 DIRECTOR: Quieten noisy cast lookup warning
b00034ffa5 DIRECTOR: Update detection entries for elroybug and trekchair
ff0d6321ec DIRECTOR: Always coerce invalid values to integer during arithmetic


Commit: acc41ee325bbfefab57365b194d476b09f8077b5
    https://github.com/scummvm/scummvm/commit/acc41ee325bbfefab57365b194d476b09f8077b5
Author: Scott Percival (code at moral.net.au)
Date: 2025-08-19T16:00:48+02:00

Commit Message:
DIRECTOR: Play sound channels stored in film loops

In addition to sprite data, you can store sound channels 1 and 2 in a
film loop. From rough experimentation these only seem to play back if
there is nothing defined in the score sound channels.

Fixes sound effects when clicking the nose and the mouth of the
big head in Eastern Mind.

Changed paths:
    engines/director/castmember/filmloop.cpp
    engines/director/castmember/filmloop.h
    engines/director/castmember/movie.cpp
    engines/director/castmember/movie.h
    engines/director/channel.cpp
    engines/director/channel.h
    engines/director/score.cpp


diff --git a/engines/director/castmember/filmloop.cpp b/engines/director/castmember/filmloop.cpp
index 54071a7c585..4c570e75d18 100644
--- a/engines/director/castmember/filmloop.cpp
+++ b/engines/director/castmember/filmloop.cpp
@@ -104,25 +104,27 @@ bool FilmLoopCastMember::isModified() {
 	return false;
 }
 
-Common::Array<Channel> *FilmLoopCastMember::getSubChannels(Common::Rect &bbox, Channel *channel) {
+Common::Array<Channel> *FilmLoopCastMember::getSubChannels(Common::Rect &bbox, uint frame) {
 	Common::Rect widgetRect(bbox.width() ? bbox.width() : _initialRect.width(), bbox.height() ? bbox.height() : _initialRect.height());
 
 	_subchannels.clear();
 
-	if (channel->_filmLoopFrame >= _frames.size()) {
-		warning("FilmLoopCastMember::getSubChannels(): Film loop frame %d requested, only %d available", channel->_filmLoopFrame, _frames.size());
+	if (frame >= _frames.size()) {
+		warning("FilmLoopCastMember::getSubChannels(): Film loop frame %d requested, only %d available", frame, _frames.size());
 		return &_subchannels;
 	}
 
 	// get the list of sprite IDs for this frame
 	Common::Array<int> spriteIds;
-	for (auto &iter : _frames[channel->_filmLoopFrame].sprites) {
-		spriteIds.push_back(iter._key);
+	for (auto &iter : _frames[frame].sprites) {
+		// channels 0 and 1 are used for audio
+		if (iter._key >= 2)
+			spriteIds.push_back(iter._key);
 	}
 	Common::sort(spriteIds.begin(), spriteIds.end());
 
 	debugC(5, kDebugImages, "FilmLoopCastMember::getSubChannels(): castId: %d, frame: %d, count: %d, initRect: %d,%d %dx%d, bbox: %d,%d %dx%d",
-			_castId, channel->_filmLoopFrame, spriteIds.size(),
+			_castId, frame, spriteIds.size(),
 			_initialRect.left + _initialRect.width()/2,
 			_initialRect.top + _initialRect.height()/2,
 			_initialRect.width(), _initialRect.height(),
@@ -132,7 +134,7 @@ Common::Array<Channel> *FilmLoopCastMember::getSubChannels(Common::Rect &bbox, C
 
 	// copy the sprites in order to the list
 	for (auto &iter : spriteIds) {
-		Sprite src = _frames[channel->_filmLoopFrame].sprites[iter];
+		Sprite src = _frames[frame].sprites[iter];
 		if (!src._cast)
 			continue;
 		// translate sprite relative to the global bounding box
@@ -172,6 +174,30 @@ Common::Array<Channel> *FilmLoopCastMember::getSubChannels(Common::Rect &bbox, C
 	return &_subchannels;
 }
 
+CastMemberID FilmLoopCastMember::getSubChannelSound1(uint frame) {
+	if (frame >= _frames.size()) {
+		warning("FilmLoopCastMember::getSubChannelSound1(): Film loop frame %d requested, only %d available", frame, _frames.size());
+		return CastMemberID();
+	}
+
+	if (_frames[frame].sprites.contains(0)) {
+		return _frames[frame].sprites[0]._castId;
+	}
+	return CastMemberID();
+}
+
+CastMemberID FilmLoopCastMember::getSubChannelSound2(uint frame) {
+	if (frame >= _frames.size()) {
+		warning("FilmLoopCastMember::getSubChannelSound2(): Film loop frame %d requested, only %d available", frame, _frames.size());
+		return CastMemberID();
+	}
+
+	if (_frames[frame].sprites.contains(1)) {
+		return _frames[frame].sprites[1]._castId;
+	}
+	return CastMemberID();
+}
+
 void FilmLoopCastMember::loadFilmLoopDataD2(Common::SeekableReadStreamEndian &stream) {
 	_initialRect = Common::Rect();
 	_frames.clear();
diff --git a/engines/director/castmember/filmloop.h b/engines/director/castmember/filmloop.h
index 762904a9ec0..5cd04417f32 100644
--- a/engines/director/castmember/filmloop.h
+++ b/engines/director/castmember/filmloop.h
@@ -22,6 +22,7 @@
 #ifndef DIRECTOR_CASTMEMBER_FILMLOOP_H
 #define DIRECTOR_CASTMEMBER_FILMLOOP_H
 
+#include "director/director.h"
 #include "director/castmember/castmember.h"
 
 namespace Director {
@@ -43,7 +44,9 @@ public:
 	bool isModified() override;
 	//Graphics::MacWidget *createWidget(Common::Rect &bbox, Channel *channel, SpriteType spriteType) override;
 
-	virtual Common::Array<Channel> *getSubChannels(Common::Rect &bbox, Channel *channel);
+	virtual Common::Array<Channel> *getSubChannels(Common::Rect &bbox, uint frame);
+	virtual CastMemberID getSubChannelSound1(uint frame);
+	virtual CastMemberID getSubChannelSound2(uint frame);
 
 	void loadFilmLoopDataD2(Common::SeekableReadStreamEndian &stream);
 	void loadFilmLoopDataD4(Common::SeekableReadStreamEndian &stream);
diff --git a/engines/director/castmember/movie.cpp b/engines/director/castmember/movie.cpp
index 4bbd43d93d4..6de14fcc714 100644
--- a/engines/director/castmember/movie.cpp
+++ b/engines/director/castmember/movie.cpp
@@ -50,13 +50,13 @@ MovieCastMember::MovieCastMember(Cast *cast, uint16 castId, MovieCastMember &sou
 	_enableScripts = source._enableScripts;
 }
 
-Common::Array<Channel> *MovieCastMember::getSubChannels(Common::Rect &bbox, Channel *channel) {
+Common::Array<Channel> *MovieCastMember::getSubChannels(Common::Rect &bbox, uint frame) {
 	if (_needsReload) {
 		_loaded = false;
 		load();
 	}
 
-	return FilmLoopCastMember::getSubChannels(bbox, channel);
+	return FilmLoopCastMember::getSubChannels(bbox, frame);
 }
 
 void MovieCastMember::load() {
diff --git a/engines/director/castmember/movie.h b/engines/director/castmember/movie.h
index 8ee275109b6..530782de96d 100644
--- a/engines/director/castmember/movie.h
+++ b/engines/director/castmember/movie.h
@@ -33,7 +33,7 @@ public:
 
 	CastMember *duplicate(Cast *cast, uint16 castId) override { return (CastMember *)(new MovieCastMember(cast, castId, *this)); }
 
-	Common::Array<Channel> *getSubChannels(Common::Rect &bbox, Channel *channel) override;
+	Common::Array<Channel> *getSubChannels(Common::Rect &bbox, uint frame) override;
 	void load() override;
 
 	bool hasField(int field) override;
diff --git a/engines/director/channel.cpp b/engines/director/channel.cpp
index 2090cb987b8..4c4f6f3fd0b 100644
--- a/engines/director/channel.cpp
+++ b/engines/director/channel.cpp
@@ -32,6 +32,7 @@
 #include "director/castmember/bitmap.h"
 #include "director/castmember/digitalvideo.h"
 #include "director/castmember/filmloop.h"
+#include "director/castmember/movie.h"
 
 #include "graphics/macgui/mactext.h"
 #include "graphics/macgui/mactextwindow.h"
@@ -741,12 +742,37 @@ bool Channel::hasSubChannels() {
 }
 
 Common::Array<Channel> *Channel::getSubChannels() {
-	if ((!_sprite->_cast) || (_sprite->_cast->_type != kCastFilmLoop && _sprite->_cast->_type != kCastMovie)) {
-		warning("Channel doesn't have any sub-channels");
-		return nullptr;
+	if (_sprite->_cast) {
+		Common::Rect bbox = getBbox();
+		if (_sprite->_cast->_type == kCastFilmLoop)
+			return ((FilmLoopCastMember *)_sprite->_cast)->getSubChannels(bbox, _filmLoopFrame);
+		else if (_sprite->_cast->_type == kCastMovie)
+			return ((MovieCastMember *)_sprite->_cast)->getSubChannels(bbox, _filmLoopFrame);
+	}
+	warning("Channel doesn't have any sub-channels");
+	return nullptr;
+}
+
+CastMemberID Channel::getSubChannelSound1() {
+	if (_sprite->_cast) {
+		if (_sprite->_cast->_type == kCastFilmLoop)
+			return ((FilmLoopCastMember *)_sprite->_cast)->getSubChannelSound1(_filmLoopFrame);
+		else if (_sprite->_cast->_type == kCastMovie)
+			return ((MovieCastMember *)_sprite->_cast)->getSubChannelSound2(_filmLoopFrame);
+	}
+	warning("Channel doesn't have any sub-channels");
+	return CastMemberID();
+}
+
+CastMemberID Channel::getSubChannelSound2() {
+	if (_sprite->_cast) {
+		if (_sprite->_cast->_type == kCastFilmLoop)
+			return ((FilmLoopCastMember *)_sprite->_cast)->getSubChannelSound1(_filmLoopFrame);
+		else if (_sprite->_cast->_type == kCastMovie)
+			return ((MovieCastMember *)_sprite->_cast)->getSubChannelSound2(_filmLoopFrame);
 	}
-	Common::Rect bbox = getBbox();
-	return ((FilmLoopCastMember *)_sprite->_cast)->getSubChannels(bbox, this);
+	warning("Channel doesn't have any sub-channels");
+	return CastMemberID();
 }
 
 } // End of namespace Director
diff --git a/engines/director/channel.h b/engines/director/channel.h
index 7360959f34b..135d5ef5bf7 100644
--- a/engines/director/channel.h
+++ b/engines/director/channel.h
@@ -95,6 +95,8 @@ public:
 	// used for film loops
 	bool hasSubChannels();
 	Common::Array<Channel> *getSubChannels();
+	CastMemberID getSubChannelSound1();
+	CastMemberID getSubChannelSound2();
 
 public:
 	Sprite *_sprite;
diff --git a/engines/director/score.cpp b/engines/director/score.cpp
index 77b266a9f24..c4e978952e1 100644
--- a/engines/director/score.cpp
+++ b/engines/director/score.cpp
@@ -1686,9 +1686,20 @@ Channel *Score::getChannelById(uint16 id) {
 
 void Score::playSoundChannel(bool puppetOnly) {
 	DirectorSound *sound = _window->getSoundManager();
+	CastMemberID sound1 = _currentFrame->_mainChannels.sound1;
+	CastMemberID sound2 = _currentFrame->_mainChannels.sound2;
+	for (int i = (int)_channels.size() - 1; i >= 0; i--) {
+		if (_channels[i]->hasSubChannels()) {
+			if (sound1.isNull())
+				sound1 = _channels[i]->getSubChannelSound1();
+			if (sound2.isNull())
+				sound2 = _channels[i]->getSubChannelSound2();
+		}
+	}
+
 	debugC(5, kDebugSound, "Score::playSoundChannel(): Sound1: %s puppet: %d type: %d, volume: %d, Sound2: %s puppet: %d, type: %d, volume: %d",
-			_currentFrame->_mainChannels.sound1.asString().c_str(), sound->isChannelPuppet(1), _currentFrame->_mainChannels.soundType1, sound->getChannelVolume(1),
-			_currentFrame->_mainChannels.sound2.asString().c_str(), sound->isChannelPuppet(2), _currentFrame->_mainChannels.soundType2, sound->getChannelVolume(2));
+			sound1.asString().c_str(), sound->isChannelPuppet(1), _currentFrame->_mainChannels.soundType1, sound->getChannelVolume(1),
+			sound2.asString().c_str(), sound->isChannelPuppet(2), _currentFrame->_mainChannels.soundType2, sound->getChannelVolume(2));
 
 	if (sound->isChannelPuppet(1)) {
 		sound->playPuppetSound(1);
@@ -1696,7 +1707,7 @@ void Score::playSoundChannel(bool puppetOnly) {
 		if (_currentFrame->_mainChannels.soundType1 >= kMinSampledMenu && _currentFrame->_mainChannels.soundType1 <= kMaxSampledMenu) {
 			sound->playExternalSound(_currentFrame->_mainChannels.soundType1, _currentFrame->_mainChannels.sound1.member, 1);
 		} else {
-			sound->playCastMember(_currentFrame->_mainChannels.sound1, 1);
+			sound->playCastMember(sound1, 1);
 		}
 	}
 
@@ -1706,7 +1717,7 @@ void Score::playSoundChannel(bool puppetOnly) {
 		if (_currentFrame->_mainChannels.soundType2 >= kMinSampledMenu && _currentFrame->_mainChannels.soundType2 <= kMaxSampledMenu) {
 			sound->playExternalSound(_currentFrame->_mainChannels.soundType2, _currentFrame->_mainChannels.sound2.member, 2);
 		} else {
-			sound->playCastMember(_currentFrame->_mainChannels.sound2, 2);
+			sound->playCastMember(sound2, 2);
 		}
 	}
 


Commit: 6e6a1dc05c7a5cdc1f3934b0e88214a1ebe916aa
    https://github.com/scummvm/scummvm/commit/6e6a1dc05c7a5cdc1f3934b0e88214a1ebe916aa
Author: Scott Percival (code at moral.net.au)
Date: 2025-08-19T16:00:48+02:00

Commit Message:
DIRECTOR: XOBJ: Add stub for Pharaohs

Changed paths:
  A engines/director/lingo/xlibs/pharaohs.cpp
  A engines/director/lingo/xlibs/pharaohs.h
    engines/director/lingo/lingo-object.cpp
    engines/director/module.mk


diff --git a/engines/director/lingo/lingo-object.cpp b/engines/director/lingo/lingo-object.cpp
index 0a83ff1aea8..0b6a807e99f 100644
--- a/engines/director/lingo/lingo-object.cpp
+++ b/engines/director/lingo/lingo-object.cpp
@@ -104,6 +104,7 @@
 #include "director/lingo/xlibs/paco.h"
 #include "director/lingo/xlibs/palxobj.h"
 #include "director/lingo/xlibs/panel.h"
+#include "director/lingo/xlibs/pharaohs.h"
 #include "director/lingo/xlibs/popupmenuxobj.h"
 #include "director/lingo/xlibs/porta.h"
 #include "director/lingo/xlibs/prefpath.h"
@@ -302,6 +303,7 @@ static const struct XLibProto {
 	XLIBDEF(PACoXObj,			kXObj,			300),	// D3
 	XLIBDEF(PalXObj,			kXObj,			400),	// D4
 	XLIBDEF(PanelXObj,			kXObj,			200),	// D2
+	XLIBDEF(PharaohsXObj,			kXObj,					400),	// D4
 	XLIBDEF(PopUpMenuXObj,		kXObj,			200),	// D2
 	XLIBDEF(Porta,				kXObj,			300),	// D3
 	XLIBDEF(PrefPath,			kXObj,			400),	// D4
diff --git a/engines/director/lingo/xlibs/pharaohs.cpp b/engines/director/lingo/xlibs/pharaohs.cpp
new file mode 100644
index 00000000000..5c2ac7238a6
--- /dev/null
+++ b/engines/director/lingo/xlibs/pharaohs.cpp
@@ -0,0 +1,106 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/system.h"
+
+#include "director/director.h"
+#include "director/lingo/lingo.h"
+#include "director/lingo/lingo-object.h"
+#include "director/lingo/lingo-utils.h"
+#include "director/lingo/xlibs/pharaohs.h"
+
+/**************************************************
+ *
+ * USED IN:
+ * gordak
+ *
+ **************************************************/
+
+/*
+-- Pharaohs External Factory. 16Feb93 PTM
+--Pharaohs
+I      mNew                --Creates a new instance of the XObject
+X      mDispose            --Disposes of XObject instance
+S      mWindowsdir         --Return the first nchars of string str
+ISSSS  mWritestring        --Write a string into an initialization file
+SSSS   mGetstring          --Get a string from an initialization file
+IS     mCheckattrib        --Check a file's attribute
+S      mCheckDrive         --Check the possible CD-ROM drive
+ */
+
+namespace Director {
+
+const char *PharaohsXObj::xlibName = "Pharaohs";
+const XlibFileDesc PharaohsXObj::fileNames[] = {
+	{ "GORDAKCD",   "gordak" },
+	{ "Pharaohs",   nullptr },
+	{ nullptr,        nullptr },
+};
+
+static MethodProto xlibMethods[] = {
+	{ "new",				PharaohsXObj::m_new,		 0, 0,	400 },
+	{ "dispose",				PharaohsXObj::m_dispose,		 0, 0,	400 },
+	{ "windowsdir",				PharaohsXObj::m_windowsdir,		 0, 0,	400 },
+	{ "writestring",				PharaohsXObj::m_writestring,		 4, 4,	400 },
+	{ "getstring",				PharaohsXObj::m_getstring,		 3, 3,	400 },
+	{ "checkattrib",				PharaohsXObj::m_checkattrib,		 1, 1,	400 },
+	{ "checkDrive",				PharaohsXObj::m_checkDrive,		 0, 0,	400 },
+	{ nullptr, nullptr, 0, 0, 0 }
+};
+
+static BuiltinProto xlibBuiltins[] = {
+
+	{ nullptr, nullptr, 0, 0, 0, VOIDSYM }
+};
+
+PharaohsXObject::PharaohsXObject(ObjectType ObjectType) :Object<PharaohsXObject>("Pharaohs") {
+	_objType = ObjectType;
+}
+
+void PharaohsXObj::open(ObjectType type, const Common::Path &path) {
+    PharaohsXObject::initMethods(xlibMethods);
+    PharaohsXObject *xobj = new PharaohsXObject(type);
+    if (type == kXtraObj)
+        g_lingo->_openXtras.push_back(xlibName);
+    g_lingo->exposeXObject(xlibName, xobj);
+    g_lingo->initBuiltIns(xlibBuiltins);
+}
+
+void PharaohsXObj::close(ObjectType type) {
+    PharaohsXObject::cleanupMethods();
+    g_lingo->_globalvars[xlibName] = Datum();
+
+}
+
+void PharaohsXObj::m_new(int nargs) {
+	g_lingo->printSTUBWithArglist("PharaohsXObj::m_new", nargs);
+	g_lingo->dropStack(nargs);
+	g_lingo->push(g_lingo->_state->me);
+}
+
+XOBJSTUBNR(PharaohsXObj::m_dispose)
+XOBJSTUB(PharaohsXObj::m_windowsdir, "C:\\WINDOWS")
+XOBJSTUB(PharaohsXObj::m_writestring, 0)
+XOBJSTUB(PharaohsXObj::m_getstring, "")
+XOBJSTUB(PharaohsXObj::m_checkattrib, -1)
+XOBJSTUB(PharaohsXObj::m_checkDrive, "D")
+
+}
diff --git a/engines/director/lingo/xlibs/pharaohs.h b/engines/director/lingo/xlibs/pharaohs.h
new file mode 100644
index 00000000000..ddda2cb7e1b
--- /dev/null
+++ b/engines/director/lingo/xlibs/pharaohs.h
@@ -0,0 +1,52 @@
+/* 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 DIRECTOR_LINGO_XLIBS_PHARAOHS_H
+#define DIRECTOR_LINGO_XLIBS_PHARAOHS_H
+
+namespace Director {
+
+class PharaohsXObject : public Object<PharaohsXObject> {
+public:
+	PharaohsXObject(ObjectType objType);
+};
+
+namespace PharaohsXObj {
+
+extern const char *xlibName;
+extern const XlibFileDesc fileNames[];
+
+void open(ObjectType type, const Common::Path &path);
+void close(ObjectType type);
+
+void m_new(int nargs);
+void m_dispose(int nargs);
+void m_windowsdir(int nargs);
+void m_writestring(int nargs);
+void m_getstring(int nargs);
+void m_checkattrib(int nargs);
+void m_checkDrive(int nargs);
+
+} // End of namespace PharaohsXObj
+
+} // End of namespace Director
+
+#endif
diff --git a/engines/director/module.mk b/engines/director/module.mk
index 2c01865c7bc..ea429650ee7 100644
--- a/engines/director/module.mk
+++ b/engines/director/module.mk
@@ -135,6 +135,7 @@ MODULE_OBJS = \
 	lingo/xlibs/paco.o \
 	lingo/xlibs/palxobj.o \
 	lingo/xlibs/panel.o \
+	lingo/xlibs/pharaohs.o \
 	lingo/xlibs/popupmenuxobj.o \
 	lingo/xlibs/porta.o \
 	lingo/xlibs/prefpath.o \


Commit: 7855a13b51bdfeeb32300a0da4a5939fa8fa8bd5
    https://github.com/scummvm/scummvm/commit/7855a13b51bdfeeb32300a0da4a5939fa8fa8bd5
Author: Scott Percival (code at moral.net.au)
Date: 2025-08-19T16:00:48+02:00

Commit Message:
DIRECTOR: Improve accuracy of path resolution

Absolute paths specified as @: and @\ are equally valid. In addition,
absolute paths that begin with a "go up" relative instruction should
ignore that aspect, i.e. "@::" resolves to "@:".

Fixes infinite loop when opening the second quiz door in Elroy Goes
Bugzerk.

Changed paths:
    engines/director/util.cpp


diff --git a/engines/director/util.cpp b/engines/director/util.cpp
index 24d36ed02e2..ad704640800 100644
--- a/engines/director/util.cpp
+++ b/engines/director/util.cpp
@@ -446,8 +446,10 @@ const char *recIndent() {
 
 bool isAbsolutePath(const Common::String &path) {
 	// Starts with Mac directory notation for the game root
-	if (path.hasPrefix(Common::String("@") + g_director->_dirSeparator))
+	if (path.hasPrefix(Common::String("@:")) ||
+		path.hasPrefix(Common::String("@\\"))) {
 		return true;
+	}
 	// Starts with a Windows drive letter
 	if (path.size() >= 3
 			&& Common::isAlpha(path[0])
@@ -472,6 +474,12 @@ Common::String rectifyRelativePath(const Common::String &path, const Common::Pat
 	Common::StringArray components = base.splitComponents();
 	uint32 idx = 0;
 
+	// If a path is provided that begins with @, it will be relative to the top level, not the base.
+	if ((path.size() > 0) && (path[0] == '@')) {
+		idx++;
+		components.clear();
+	}
+
 	while (idx < path.size()) {
 		uint32 start = idx;
 		while (idx < path.size() && path[idx] != ':' && path[idx] != '\\')
@@ -539,7 +547,8 @@ Common::String convertPath(const Common::String &path) {
 
 	if (path.hasPrefix("::")) { // Parent directory
 		idx = 2;
-	} else if (path.hasPrefix(Common::String("@") + g_director->_dirSeparator)) { // Root of the game
+	} else if (path.hasPrefix(Common::String("@:")) ||
+				path.hasPrefix(Common::String("@\\"))) { // Root of the game
 		idx = 2;
 	} else if (path.size() >= 3
 					&& Common::isAlpha(path[0])


Commit: 523b2d63c927b43b92b3a211246c30b831fea54c
    https://github.com/scummvm/scummvm/commit/523b2d63c927b43b92b3a211246c30b831fea54c
Author: Scott Percival (code at moral.net.au)
Date: 2025-08-19T16:00:48+02:00

Commit Message:
DIRECTOR: LINGO: closeXlib() does not affect Xtras

Fixes loading sequence of Star Trek Captain's Chair.

Changed paths:
    engines/director/lingo/lingo-object.cpp


diff --git a/engines/director/lingo/lingo-object.cpp b/engines/director/lingo/lingo-object.cpp
index 0b6a807e99f..b564dc58ad6 100644
--- a/engines/director/lingo/lingo-object.cpp
+++ b/engines/director/lingo/lingo-object.cpp
@@ -446,7 +446,10 @@ void Lingo::closeXLib(Common::String name) {
 
 void Lingo::closeOpenXLibs() {
 	for (auto &it : _openXLibs) {
-		closeXLib(it._key);
+		// does not affect Xtras
+		if (it._value == kXObj) {
+			closeXLib(it._key);
+		}
 	}
 }
 


Commit: 63a6898b1afc9014d240e7e0d90f5ad39c8c6207
    https://github.com/scummvm/scummvm/commit/63a6898b1afc9014d240e7e0d90f5ad39c8c6207
Author: Scott Percival (code at moral.net.au)
Date: 2025-08-19T16:00:48+02:00

Commit Message:
DIRECTOR: Add detection patch for GORD at K

Changed paths:
    engines/director/lingo/lingo-patcher.cpp


diff --git a/engines/director/lingo/lingo-patcher.cpp b/engines/director/lingo/lingo-patcher.cpp
index da0f710ee83..fa29e52b05c 100644
--- a/engines/director/lingo/lingo-patcher.cpp
+++ b/engines/director/lingo/lingo-patcher.cpp
@@ -481,6 +481,15 @@ on getRes\r\
 end\r\
 ";
 
+/*
+ * GORD at K has a complicated CD detection method which includes writing a temp file to the CD
+ * drive. Since this always works, we have to stub out the entire method.
+ */
+const char *const gordakDetectionFix = "\
+on checkFiles\r\
+   go to movie \"gordak\\intro.dxr\"\r\
+end\r\
+";
 
 struct ScriptHandlerPatch {
 	const char *gameId;
@@ -524,6 +533,7 @@ struct ScriptHandlerPatch {
 	{"mcmillennium", nullptr, kPlatformMacintosh, "Mission Code Millennium:Mission Code Millennium", kMovieScript, 15, DEFAULT_CAST_LIB, &mcmillenniumResDetectionFix},
 	{"mcmillennium", nullptr, kPlatformWindows, "PC\\SHARED.DXR", kMovieScript, 1013, DEFAULT_CAST_LIB, &mcmillenniumDriveDetectionFix},
 	{"mcmillennium", nullptr, kPlatformMacintosh, "Mission Code Millennium:SHARED.Dxr", kMovieScript, 1013, DEFAULT_CAST_LIB, &mcmillenniumDriveDetectionFix},
+	{"gordak", nullptr, kPlatformWindows, "GORDAKCD.EXE", kMovieScript, 2, DEFAULT_CAST_LIB, &gordakDetectionFix},
 	{nullptr, nullptr, kPlatformUnknown, nullptr, kNoneScript, 0, 0, nullptr},
 
 };


Commit: 6c3f823fafe3fb68348eda0e04c01e5b77121a49
    https://github.com/scummvm/scummvm/commit/6c3f823fafe3fb68348eda0e04c01e5b77121a49
Author: Scott Percival (code at moral.net.au)
Date: 2025-08-19T16:00:48+02:00

Commit Message:
DIRECTOR: LINGO: Improve accuracy of b_min/b_max

For the most part, b_min/b_max behaviour can be explained away by doing
a standard less-than/greater-than comparison on each element in
sequence.

Except... there's a whole boatload of edge cases related to including a VOID,
which can make the answer change based on the order of the elements.

Also there are specific bad things that happen when you include a string
before or after a VOID.

Oh! And D4 has a different set of awful rules compared to D5+.

Fixes scrolling around with the night-vision helmet in Elroy Goes
Bugzerk.

Changed paths:
    engines/director/lingo/lingo-builtins.cpp
    engines/director/lingo/lingo.cpp
    engines/director/lingo/tests/equality.lingo
    engines/director/lingo/tests/math.lingo


diff --git a/engines/director/lingo/lingo-builtins.cpp b/engines/director/lingo/lingo-builtins.cpp
index 0855953dba1..959a1218b7b 100644
--- a/engines/director/lingo/lingo-builtins.cpp
+++ b/engines/director/lingo/lingo-builtins.cpp
@@ -1144,36 +1144,57 @@ void LB::b_max(int nargs) {
 	max.type = INT;
 	max.u.i = 0;
 
+	bool hasVoidQuirk = g_director->getVersion() < 500;
+
+	Common::Array<Datum> testArr;
+
 	if (nargs == 1) {
 		Datum d = g_lingo->pop();
 		if (d.type == ARRAY) {
-			uint arrsize = d.u.farr->arr.size();
-
-			if (d.u.farr->_sorted && arrsize) {
-				max = d.u.farr->arr[arrsize - 1];
-			} else {
-				for (uint i = 0; i < arrsize; i++) {
-					Datum item = d.u.farr->arr[i];
-					if (i == 0 || item > max) {
-						max = item;
-					}
-				}
+			for (auto &it : d.u.farr->arr) {
+				testArr.push_back(it);
 			}
 		} else {
 			max = d;
 		}
 	} else if (nargs > 0) {
 		for (int i = 0; i < nargs; i++) {
-			Datum d = g_lingo->_state->stack[g_lingo->_state->stack.size() - nargs + i];
+			Datum d = g_lingo->peek(nargs - i - 1);
 			if (d.type == ARRAY) {
 				warning("b_max: undefined behavior: array mixed with other args");
 			}
-			if (i == 0 || d > max) {
-				max = d;
-			}
+			testArr.push_back(d);
 		}
 		g_lingo->dropStack(nargs);
 	}
+
+	if (!testArr.empty()) {
+		// D4: if there is a VOID for the first arg of max, return VOID
+		if (hasVoidQuirk && testArr[0].type == VOID) {
+			g_lingo->pushVoid();
+			return;
+		}
+		// The trick seems to be to compare each item in sequence.
+		// D4: If we encounter a VOID, treat it as the smallest value possible.
+		// D5+: If we encounter a string, and the current maximum is a VOID, it gets ignored.
+		// D5+: If we encounter a VOID, and the current maximum is a STRING, set it to VOID.
+
+		max = testArr[0];
+		for (int i = 1; i < (int)testArr.size(); i++) {
+			if (!hasVoidQuirk) {
+				if ((max.type == VOID) && (testArr[i].type == STRING)) {
+					continue;
+				} else if ((testArr[i].type == VOID) && (max.type == STRING)) {
+					max = Datum();
+					continue;
+				}
+			}
+			if (testArr[i] > max) {
+				max = testArr[i];
+			}
+		}
+	}
+
 	g_lingo->push(max);
 }
 
@@ -1182,36 +1203,58 @@ void LB::b_min(int nargs) {
 	min.type = INT;
 	min.u.i = 0;
 
+	bool hasVoidQuirk = g_director->getVersion() < 500;
+
+	Common::Array<Datum> testArr;
+
 	if (nargs == 1) {
 		Datum d = g_lingo->pop();
 		if (d.type == ARRAY) {
-			uint arrsize = d.u.farr->arr.size();
-
-			if (d.u.farr->_sorted && arrsize) {
-				min = d.u.farr->arr[0];
-			} else {
-				for (uint i = 0; i < arrsize; i++) {
-					Datum item = d.u.farr->arr[i];
-					if (i == 0 || item < min) {
-						min = item;
-					}
-				}
+			for (auto &it : d.u.farr->arr) {
+				testArr.push_back(it);
 			}
 		} else {
 			min = d;
 		}
 	} else if (nargs > 0) {
 		for (int i = 0; i < nargs; i++) {
-			Datum d = g_lingo->_state->stack[g_lingo->_state->stack.size() - nargs + i];
+			Datum d = g_lingo->peek(nargs - i - 1);
 			if (d.type == ARRAY) {
 				warning("b_min: undefined behavior: array mixed with other args");
 			}
-			if (i == 0 || d < min) {
-				min = d;
-			}
+			testArr.push_back(d);
 		}
 		g_lingo->dropStack(nargs);
 	}
+	if (!testArr.empty()) {
+		// D4: if there is a VOID for the last arg of min, return VOID
+		if (hasVoidQuirk && testArr[testArr.size()-1].type == VOID) {
+			g_lingo->pushVoid();
+			return;
+		}
+		// The trick seems to be to compare each item in sequence.
+		// If we encounter a VOID, and the current minimum is a number or a symbol, it gets converted to VOID.
+		// If we encounter a VOID, and the current minimum is a string, we ignore it.
+		// If the current minimum is VOID, and we encounter a string, the current minimum is set to the string.
+
+		min = testArr[0];
+		for (int i = 1; i < (int)testArr.size(); i++) {
+			if (testArr[i].type == VOID) {
+				if (min.type != STRING) {
+					min = Datum();
+				}
+				continue;
+			// D4: if the current minimum is VOID, set the next value as the new minimum
+			// D5+: if the current minimum is VOID, and the next value is a string (coercable or not), set it as the new minimum
+			} else if ((min.type == VOID) && (hasVoidQuirk || (testArr[i].type == STRING))) {
+				min = testArr[i];
+				continue;
+			}
+			if (testArr[i] < min) {
+				min = testArr[i];
+			}
+		}
+	}
 	g_lingo->push(min);
 }
 
diff --git a/engines/director/lingo/lingo.cpp b/engines/director/lingo/lingo.cpp
index f37b4d0b479..987dec4a490 100644
--- a/engines/director/lingo/lingo.cpp
+++ b/engines/director/lingo/lingo.cpp
@@ -858,9 +858,6 @@ int Lingo::getAlignedType(const Datum &d1, const Datum &d2, bool equality) {
 		opType = FLOAT;
 	} else if ((d1Type == STRING && d2Type == INT) || (d1Type == INT && d2Type == STRING)) {
 		opType = STRING;
-	} else if ((d1Type == SYMBOL && d2Type != SYMBOL) || (d2Type == SYMBOL && d1Type != SYMBOL)) {
-		// some fun undefined behaviour: adding anything to a symbol returns an int.
-		opType = INT;
 	} else if (d1Type == d2Type) {
 		opType = d1Type;
 	}
@@ -1525,6 +1522,13 @@ uint32 Datum::compareTo(const Datum &d) const {
 			}
 		}
 		return result;
+
+		// non-coercable strings always outrank numbers and VOID
+	} else if ((this->type == FLOAT || this->type == INT || this->type == VOID) && (d.type == STRING || d.type == SYMBOL)) {
+		return kCompareLessEqual | kCompareLess;
+	} else if ((d.type == FLOAT || d.type == INT || d.type == VOID) && (this->type == STRING || this->type == SYMBOL)) {
+		return kCompareGreaterEqual | kCompareGreater;
+
 	} else {
 		warning("Datum::compareTo(): Invalid comparison between types %s and %s", type2str(), d.type2str());
 		return kCompareError;
diff --git a/engines/director/lingo/tests/equality.lingo b/engines/director/lingo/tests/equality.lingo
index b13f5104462..ea8c1492604 100644
--- a/engines/director/lingo/tests/equality.lingo
+++ b/engines/director/lingo/tests/equality.lingo
@@ -58,6 +58,15 @@ scummvmAssert("2000" < "25")
 scummvmAssert("abc" < "abcd")
 scummvmAssert("abc" < "def")
 
+-- Non-coercable string is always bigger than a number or void
+scummvmAssert("test" > 3000)
+scummvmAssert(3000 < "test")
+scummvmAssert("test" > 300.0)
+scummvmAssert(300.0 < "test")
+scummvmAssert("test" > VOID)
+scummvmAssert(VOID < "test")
+
+
 -- Mimic an object
 scummvmAssert("<Object:#FileIO" > 0)
 
diff --git a/engines/director/lingo/tests/math.lingo b/engines/director/lingo/tests/math.lingo
index 3c2d8f1adf6..94ce5eccddf 100644
--- a/engines/director/lingo/tests/math.lingo
+++ b/engines/director/lingo/tests/math.lingo
@@ -73,3 +73,93 @@ scummvmAssertEqual(integer(-2.49), -1)
 scummvmAssertEqual(integer(-2.5), -2)
 
 set the scummvmVersion to save
+
+-- Min/max - D4 has bugs related to handling VOID
+set the scummvmVersion to 400
+scummvmAssertEqual(min(VOID, 30), 30)
+scummvmAssertEqual(ilk(max(VOID, 30)), #void)
+scummvmAssertEqual(ilk(min(30, VOID)), #void)
+scummvmAssertEqual(max(30, VOID), 30)
+
+scummvmAssertEqual(min(VOID, "test"), "test")
+scummvmAssertEqual(ilk(max(VOID, "test")), #void)
+scummvmAssertEqual(ilk(min("test", VOID)), #void)
+scummvmAssertEqual(max("test", VOID), "test")
+
+scummvmAssertEqual(min(1, VOID, 3), 3)
+scummvmAssertEqual(max(1, VOID, 3), 3)
+scummvmAssertEqual(min(VOID, 3, "test"), 3)
+scummvmAssertEqual(ilk(max(VOID, 3, "test")), #void)
+scummvmAssertEqual(min(1, VOID, 3, "test"), 3)
+scummvmAssertEqual(max(1, VOID, 3, "test"), "test")
+
+scummvmAssertEqual(min(VOID, "test", 3), 3)
+scummvmAssertEqual(ilk(max(VOID, "test", 3)), #void)
+scummvmAssertEqual(min(3, VOID, "test"), "test")
+scummvmAssertEqual(max(3, VOID, "test"), "test")
+scummvmAssertEqual(ilk(min(3, "test", VOID)), #void)
+scummvmAssertEqual(max(3, "test", VOID), "test")
+scummvmAssertEqual(ilk(min("test", 3, VOID)), #void)
+scummvmAssertEqual(max("test", 3, VOID), "test")
+scummvmAssertEqual(min("test", VOID, 3), 3)
+scummvmAssertEqual(max("test", VOID, 3), "test")
+scummvmAssertEqual(min("test", VOID, 3, "2.5"), "2.5")
+scummvmAssertEqual(max("test", VOID, 3, "2.5"), "test")
+scummvmAssertEqual(min(1, "test", VOID, 3, "2.5"), "2.5")
+scummvmAssertEqual(max(1, "test", VOID, 3, "2.5"), "test")
+scummvmAssertEqual(min(1, "test", 3, "2.5"), 1)
+scummvmAssertEqual(max(1, "test", 3, "2.5"), "test")
+
+-- D5 fixes the VOID bug
+set the scummvmVersion to 500
+scummvmAssertEqual(ilk(min(VOID, 30)), #void)
+scummvmAssertEqual(max(VOID, 30), 30)
+scummvmAssertEqual(ilk(min(30, VOID)), #void)
+scummvmAssertEqual(max(30, VOID), 30)
+
+scummvmAssertEqual(min(VOID, "test"), "test")
+scummvmAssertEqual(ilk(max(VOID, "test")), #void)
+scummvmAssertEqual(min("test", VOID), "test")
+scummvmAssertEqual(ilk(max("test", VOID)), #void)
+
+scummvmAssertEqual(ilk(min(1, VOID, 3)), #void)
+scummvmAssertEqual(max(1, VOID, 3), 3)
+scummvmAssertEqual(min(VOID, 3, "test"), "test")
+scummvmAssertEqual(max(VOID, 3, "test"), "test")
+scummvmAssertEqual(min(1, VOID, 3, "test"), "test")
+scummvmAssertEqual(max(1, VOID, 3, "test"), "test")
+
+scummvmAssertEqual(min(VOID, "test", 3), 3)
+scummvmAssertEqual(max(VOID, "test", 3), 3)
+scummvmAssertEqual(min(3, VOID, "test"), "test")
+scummvmAssertEqual(max(3, VOID, "test"), "test")
+scummvmAssertEqual(ilk(min(3, "test", VOID)), #void)
+scummvmAssertEqual(ilk(max(3, "test", VOID)), #void)
+scummvmAssertEqual(ilk(min("test", 3, VOID)), #void)
+scummvmAssertEqual(ilk(max("test", 3, VOID)), #void)
+scummvmAssertEqual(min("test", VOID, 3), 3)
+scummvmAssertEqual(max("test", VOID, 3), 3)
+scummvmAssertEqual(min("test", VOID, 3, "2.5"), "2.5")
+scummvmAssertEqual(max("test", VOID, 3, "2.5"), 3)
+scummvmAssertEqual(min(1, "test", VOID, 3, "2.5"), "2.5")
+scummvmAssertEqual(max(1, "test", VOID, 3, "2.5"), 3)
+scummvmAssertEqual(min(1, "test", 3, "2.5"), 1)
+scummvmAssertEqual(max(1, "test", 3, "2.5"), "test")
+
+-- cases the same for both
+set the scummvmVersion to 400
+scummvmAssertEqual(min(29, "30.0"), 29)
+scummvmAssertEqual(max(29, "30.0"), "30.0")
+scummvmAssertEqual(min(30, "30.0"), 30)
+scummvmAssertEqual(max(30, "30.0"), "30.0")
+scummvmAssertEqual(min(31, "30.0"), "30.0")
+scummvmAssertEqual(max(31, "30.0"), 31)
+scummvmAssertEqual(min(30.0, "30.1"), 30.0)
+scummvmAssertEqual(max(30.0, "30.1"), "30.1")
+scummvmAssertEqual(min(30.2, "30.1"), "30.1")
+scummvmAssertEqual(max(30.2, "30.1"), 30.2)
+
+scummvmAssertEqual(min(50000, "test"), 50000)
+scummvmAssertEqual(max(50000, "test"), "test")
+scummvmAssertEqual(min(5000.0, "test"), 5000.0)
+scummvmAssertEqual(max(5000.0, "test"), "test")


Commit: 111f38cb295701f19f4e50bf01cdcd76e878f0c8
    https://github.com/scummvm/scummvm/commit/111f38cb295701f19f4e50bf01cdcd76e878f0c8
Author: Scott Percival (code at moral.net.au)
Date: 2025-08-19T16:00:48+02:00

Commit Message:
DIRECTOR: Quieten noisy cast lookup warning

Changed paths:
    engines/director/movie.cpp


diff --git a/engines/director/movie.cpp b/engines/director/movie.cpp
index cb0deacbe6d..b78f28a5253 100644
--- a/engines/director/movie.cpp
+++ b/engines/director/movie.cpp
@@ -483,6 +483,8 @@ bool Movie::loadCastLibFrom(uint16 libId, Common::Path &filename) {
 CastMember *Movie::getCastMember(CastMemberID memberID) {
 	CastMember *result = nullptr;
 	if (_casts.contains(memberID.castLib)) {
+		if (memberID.member == 0)
+			return nullptr;
 		result = _casts.getVal(memberID.castLib)->getCastMember(memberID.member);
 		if (result == nullptr && _sharedCast) {
 			result = _sharedCast->getCastMember(memberID.member);


Commit: b00034ffa54d8324cbe68a4571fdf2f4949cb9d9
    https://github.com/scummvm/scummvm/commit/b00034ffa54d8324cbe68a4571fdf2f4949cb9d9
Author: Scott Percival (code at moral.net.au)
Date: 2025-08-19T16:00:48+02:00

Commit Message:
DIRECTOR: Update detection entries for elroybug and trekchair

Changed paths:
    engines/director/detection_tables.h


diff --git a/engines/director/detection_tables.h b/engines/director/detection_tables.h
index 485d317bd79..be33d271ee4 100644
--- a/engines/director/detection_tables.h
+++ b/engines/director/detection_tables.h
@@ -4188,6 +4188,8 @@ static const DirectorGameDescription gameDescriptions[] = {
 	WINGAME1("elroybug", "", "BUGZERK.EXE", "t:3be9fa389257a608ff21e09074355fa5", 1593811, 400),
 	MACGAME1("elroybug", "1.0", "Elroy Goes Bugzerk", "r:cbce20666bfe47a9533331c6be1e6039", 321438, 404),
 	WINGAME1("elroybug", "1.0", "BUGZERK.EXE", "t:7fcb09d5dcc8096cd7ffa1fd618e5500", 1561075, 404),
+	MACGAME1_l("elroybug", "1.0", "xn--Elroy jagt den Technokfer-6ec", "r:bcd3c718db258701496b3c5bcb827ef2", 520582, Common::DE_DEU, 404),
+	WINGAME1_l("elroybug", "1.0", "BUGZERK.EXE", "t:ce2739427840ffc3bdc1d59d38f3c80d", 1521691, Common::DE_DEU, 404),
 	MACDEMO1("elroybug", "Demo", "Elroy Goes Bugzerk Demo", "bcd3c718db258701496b3c5bcb827ef2", 498394, 404),
 	WINDEMO1("elroybug", "Demo", "ELRYDEMO.EXE", "cb2d86ea52d81d12d1fe8eadfb4a118c", 2438763, 404),
 
@@ -7857,8 +7859,6 @@ static const DirectorGameDescription gameDescriptions[] = {
 	// Found on Revolutionary War Picture CD from Holiday Digital Pictures
 	MACGAME1("ssrevwar", "", "Revolutionary War Screen Saver", "r:43234754a346ed7ac25b581f6d106866", 217838, 500),
 
-	WINGAME1("trekchair", "", "Cap_win.exe", "a28313a078c0cd3cebdf505af1d63d88", 1399089, 400),
-
 	// Mac version requires installation, Install Star Warped, VISE
 	// Preview is from X-Fools disc
 	// Win/Mac previews from Microshaft Winblows are D6
@@ -7934,8 +7934,8 @@ static const DirectorGameDescription gameDescriptions[] = {
 	MACGAME1("treasuresamnh", "", "Treasures of the AMNH/Treasures", "r:d17d1380e3d87863c406e012bd5d8078", 718945, 501),
 	WINGAME1("treasuresamnh", "", "TAMNH/TAMNH.EXE", "t:b8417ced47c60179e0ca1cf59bfc209b", 1641329, 501),
 
-	MACGAME1("trekchair", "", "xn--Star Trek Captain's Chair-tl1p/Captain's Chair Player", "d8bad538d97edf5990c451699e429db3", 764476, 501),
-	WINGAME1t("trekchair", "", "CAP_WIN.EXE", "6c3c66dd2a5a91257fd2691e3888d47d", 1399089, 500),
+	MACGAME1("trekchair", "1.0", "xn--Star Trek Captain's Chair-tl1p/Captain's Chair Player", "r:d8bad538d97edf5990c451699e429db3", 764476, 501),
+	WINGAME1("trekchair", "1.0", "SOURCE/CAP_WIN.EXE", "t:6c3c66dd2a5a91257fd2691e3888d47d", 1399089, 500),
 
 	MACGAME1("troubleshoot101", "Basic", "Troubleshooting 101 IA",		   "r:62e979424add2428daa835610fb83864", 719005, 501),
 	MACGAME1("troubleshoot101", "Music", "Troubleshooting 101 IA w MUSIC", "r:222fbd020a3910ef748724945145771c", 719005, 501),


Commit: ff0d6321ec8d61e273742071ee5347ae74182dd4
    https://github.com/scummvm/scummvm/commit/ff0d6321ec8d61e273742071ee5347ae74182dd4
Author: Scott Percival (code at moral.net.au)
Date: 2025-08-19T16:00:48+02:00

Commit Message:
DIRECTOR: Always coerce invalid values to integer during arithmetic

Changed paths:
    engines/director/lingo/lingo-code.cpp
    engines/director/lingo/lingo.cpp
    engines/director/lingo/tests/strings.lingo


diff --git a/engines/director/lingo/lingo-code.cpp b/engines/director/lingo/lingo-code.cpp
index 6056d556fa0..ce709b81cc8 100644
--- a/engines/director/lingo/lingo-code.cpp
+++ b/engines/director/lingo/lingo-code.cpp
@@ -750,7 +750,8 @@ Datum LC::addData(Datum &d1, Datum &d2) {
 	} else if (alignedType == INT) {
 		res = Datum(d1.asInt() + d2.asInt());
 	} else {
-		g_lingo->lingoError("LC::addData(): not supported between types %s and %s", d1.type2str(), d2.type2str());
+		res = Datum(d1.asInt() + d2.asInt());
+		warning("LC::addData(): not supported between types %s and %s", d1.type2str(), d2.type2str());
 	}
 	return res;
 }
@@ -779,7 +780,8 @@ Datum LC::subData(Datum &d1, Datum &d2) {
 	} else if (alignedType == INT) {
 		res = Datum(d1.asInt() - d2.asInt());
 	} else {
-		g_lingo->lingoError("LC::subData(): not supported between types %s and %s", d1.type2str(), d2.type2str());
+		res = Datum(d1.asInt() - d2.asInt());
+		warning("LC::subData(): not supported between types %s and %s", d1.type2str(), d2.type2str());
 	}
 	return res;
 }
@@ -808,7 +810,8 @@ Datum LC::mulData(Datum &d1, Datum &d2) {
 	} else if (alignedType == INT) {
 		res = Datum(d1.asInt() * d2.asInt());
 	} else {
-		g_lingo->lingoError("LC::mulData(): not supported between types %s and %s", d1.type2str(), d2.type2str());
+		res = Datum(d1.asInt() * d2.asInt());
+		warning("LC::mulData(): not supported between types %s and %s", d1.type2str(), d2.type2str());
 	}
 	return res;
 }
@@ -831,7 +834,7 @@ Datum LC::divData(Datum &d1, Datum &d2) {
 
 	if ((d2.type == INT && d2.u.i == 0) ||
 			(d2.type == FLOAT && d2.u.f == 0.0)) {
-		warning("LC::divData(): division by zero");
+		g_lingo->lingoError("LC::divData(): division by zero");
 		d2 = Datum(1);
 	}
 
@@ -846,7 +849,12 @@ Datum LC::divData(Datum &d1, Datum &d2) {
 	} else if (alignedType == INT) {
 		res = Datum(d1.asInt() / d2.asInt());
 	} else {
-		g_lingo->lingoError("LC::divData(): not supported between types %s and %s", d1.type2str(), d2.type2str());
+		int denom = d2.asInt();
+		if (denom == 0) {
+			g_lingo->lingoError("LC::divData(): division by zero");
+		}
+		res = Datum(d1.asInt() / d2.asInt());
+		warning("LC::divData(): not supported between types %s and %s", d1.type2str(), d2.type2str());
 	}
 
 	return res;
@@ -866,8 +874,8 @@ Datum LC::modData(Datum &d1, Datum &d2) {
 	int i1 = d1.asInt();
 	int i2 = d2.asInt();
 	if (i2 == 0) {
-		g_lingo->lingoError("LC::modData(): division by zero");
-		i2 = 1;
+		warning("LC::modData(): division by zero");
+		return Datum(0);
 	}
 
 	Datum res(i1 % i2);
@@ -900,7 +908,8 @@ Datum LC::negateData(Datum &d) {
 	} else if (d.type == VOID) {
 		res = Datum(0);
 	} else {
-		g_lingo->lingoError("LC::negateData(): not supported for type %s", d.type2str());
+		warning("LC::negateData(): not supported for type %s", d.type2str());
+		res = Datum(-d.asInt());
 	}
 
 	return res;
diff --git a/engines/director/lingo/lingo.cpp b/engines/director/lingo/lingo.cpp
index 987dec4a490..292080dd1f0 100644
--- a/engines/director/lingo/lingo.cpp
+++ b/engines/director/lingo/lingo.cpp
@@ -1060,6 +1060,7 @@ int Datum::asInt() const {
 
 	switch (type) {
 	case STRING:
+	case SYMBOL:
 		{
 			Common::String src = asString();
 			char *endPtr = nullptr;
@@ -1085,11 +1086,6 @@ int Datum::asInt() const {
 			res = (int)u.f;
 		}
 		break;
-	case SYMBOL:
-		// Undefined behaviour, but relied on by bad game code that e.g. adds things to symbols.
-		// Return a 32-bit number that's sort of related.
-		res = (int)((uint64)u.s & 0xffffffffL);
-		break;
 	default:
 		warning("Incorrect operation asInt() for type: %s", type2str());
 	}
diff --git a/engines/director/lingo/tests/strings.lingo b/engines/director/lingo/tests/strings.lingo
index 27e844caa23..8f71c024543 100644
--- a/engines/director/lingo/tests/strings.lingo
+++ b/engines/director/lingo/tests/strings.lingo
@@ -76,13 +76,13 @@ scummvmAssertEqual("    2" + 5, 7.0)
 scummvmAssertEqual("    2.5" + 5, 7.5)
 
 -- non number strings should coerce to a pointer
-scummvmAssert("incorrect" + 5 > 10000)
-scummvmAssert("    2.5     " + 5 > 10000)
-scummvmAssert("2 uhhh" + 5 > 10000)
-scummvmAssert("2.5 uhhh" + 5 > 10000)
+scummvmAssertEqual(ilk("incorrect" + 5), #integer)
+scummvmAssertEqual(ilk("    2.5     " + 5), #integer)
+scummvmAssert(ilk("2 uhhh" + 5), #integer)
+scummvmAssert(ilk("2.5 uhhh" + 5), #integer)
 put "sausages" into testString
 put (testString + 0) into testPointer
-scummvmAssertEqual(testPointer > 10000, TRUE)
+scummvmAssertEqual(ilk(testPointer), #integer)
 scummvmAssertEqual(testString + 4, testPointer + 4)
 scummvmAssertEqual(testString - 4, testPointer - 4)
 scummvmAssertEqual(testString * 4, testPointer * 4)
@@ -90,7 +90,7 @@ scummvmAssertEqual(testString / 4, testPointer / 4)
 -- same horrible logic should apply to symbols
 put #haggis into testString
 put (testString + 0) into testPointer
-scummvmAssertEqual(testPointer > 10000, TRUE)
+scummvmAssertEqual(ilk(testPointer), #integer)
 scummvmAssertEqual(testString + 4, testPointer + 4)
 scummvmAssertEqual(testString - 4, testPointer - 4)
 scummvmAssertEqual(testString * 4, testPointer * 4)




More information about the Scummvm-git-logs mailing list