[Scummvm-git-logs] scummvm master -> 13fa4baf7288785f812246083768d592ff3523ba

sev- noreply at scummvm.org
Sun May 12 20:00:36 UTC 2024


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

Summary:
4958eef30a DIRECTOR: XOBJ: Fix typo in InstObj
7f6a4d80ca DIRECTOR: XOBJ: Add first pass at video mixer to MMovie
ec56a7c2fe DIRECTOR: Fix kTheCursor output after Cursor::readFromCast
f8210d9b58 DIRECTOR: Fix cast member name cache
9ac91fc68c DIRECTOR: XOBJ: Fix UnitTest calling convention to match DLL
45c9abff93 DIRECTOR: Refactor Channel::getMask()
61d0603304 DIRECTOR: Duplicate cast member fixes
e28f30d680 DIRECTOR: XOBJ: Add readFile/writeFile stubs to MMovie
e0242d8d6a DIRECTOR: Make b_random return 16-bit numbers
d7461b2f48 DIRECTOR: LINGO: Fix array eq/neq comparison bugs
5cacbd49e3 DIRECTOR: Fix Frame::readSprite debug messages
d1a739fbce DIRECTOR: Fix scaling of sprites in score frames
0bbed1793e COMMON: Silence FSDirectory::createReadStreamForMemberAltStream warning
51b7d37e70 DIRECTOR: Fix kTheDuration of DigitalVideoCastMember
ed4d4e355b DIRECTOR: Fix absolute paths in linked cast members
a08934a95e DIRECTOR: Update current frame before running startMovie
32f3cd0633 DIRECTOR: Add more debugging to FilmLoopCastMember
1bb44c5241 DIRECTOR: Add locH and locV property support for POINT
64cc8adb3c DIRECTOR: LINGO: Allow b_length(VOID)
10ac6b61d8 DIRECTOR: LINGO: Add guardrail for implicit factory calls
785fb478b6 DIRECTOR: Fix null pointer in BitmapCastMember
f2a23a5e39 DIRECTOR: Add cheat-enabling patch for Virtual Nightclub
ce447005da DIRECTOR: XOBJ: MMovie fixes
8b2a30dd65 DIRECTOR: Rename Lingo::printSTUBWithArglist
cabbd1288b DIRECTOR: XOBJ: Add new UnitTest methods
e4b90dd234 DIRECTOR: Use mouse coordinates from individual events
6a0fb37616 DIRECTOR: Allow only one input event to execute at a time
df0ccad5dc DIRECTOR: Add buffer for injecting input events
aa15020b8d DIRECTOR: Refactor mouse event handling
8e84fa687e DIRECTOR: LINGO: Handle arithmetic attempted on cast references
3bcc920a1e DIRECTOR: XOBJ: Have MMovie cache the last drawn frame
5cd47cec38 DIRECTOR: LINGO: Fix Datum collision with b_abs
60f1a24c81 DIRECTOR: LINGO: Add support for script objects to some PARRAY methods
428ad1ac18 DIRECTOR: Fix Cursor::readFromCast for D5
74efd8e6ea DIRECTOR: Fixes based on review feedback
1c07d3e9aa DIRECTOR: Move save modal glue into function
a1d692963d DIRECTOR: LINGO: Track ScriptContext properties in addition order
13fa4baf72 DIRECTOR: Split savename prefix into common method


Commit: 4958eef30ae89afb2f50f72e694c3331a7738e6d
    https://github.com/scummvm/scummvm/commit/4958eef30ae89afb2f50f72e694c3331a7738e6d
Author: Scott Percival (code at moral.net.au)
Date: 2024-05-12T22:00:19+02:00

Commit Message:
DIRECTOR: XOBJ: Fix typo in InstObj

Changed paths:
    engines/director/lingo/xlibs/instobj.cpp


diff --git a/engines/director/lingo/xlibs/instobj.cpp b/engines/director/lingo/xlibs/instobj.cpp
index 974264d5d6e..ee81d29023c 100644
--- a/engines/director/lingo/xlibs/instobj.cpp
+++ b/engines/director/lingo/xlibs/instobj.cpp
@@ -124,7 +124,7 @@ void InstObjXObj::m_getDriveType(int nargs) {
 	g_lingo->printSTUBWithArglist("InstObjXObj::m_getDriveType", nargs);
 	Datum result("Undetermined Drive Type");
 
-	if (nargs == 1) {
+	if (nargs != 1) {
 		warning("InstObjXObj: expected 1 argument");
 		g_lingo->dropStack(nargs);
 	} else {


Commit: 7f6a4d80ca8b4cdee410c432e213d97b2defcb5c
    https://github.com/scummvm/scummvm/commit/7f6a4d80ca8b4cdee410c432e213d97b2defcb5c
Author: Scott Percival (code at moral.net.au)
Date: 2024-05-12T22:00:19+02:00

Commit Message:
DIRECTOR: XOBJ: Add first pass at video mixer to MMovie

Makes initial cutscenes playable in Virtual Nightclub.

Changed paths:
    engines/director/lingo/xlibs/mmovie.cpp
    engines/director/lingo/xlibs/mmovie.h


diff --git a/engines/director/lingo/xlibs/mmovie.cpp b/engines/director/lingo/xlibs/mmovie.cpp
index 4842e862644..47bcd8dc990 100644
--- a/engines/director/lingo/xlibs/mmovie.cpp
+++ b/engines/director/lingo/xlibs/mmovie.cpp
@@ -19,7 +19,9 @@
  *
  */
 
+#include "common/file.h"
 #include "common/system.h"
+#include "video/qt_decoder.h"
 
 #include "director/director.h"
 #include "director/lingo/lingo.h"
@@ -112,6 +114,107 @@ MMovieXObject::MMovieXObject(ObjectType ObjectType) :Object<MMovieXObject>("MMov
 	_objType = ObjectType;
 }
 
+bool MMovieXObject::playSegment(int movieIndex, int segIndex, bool looping, bool restore, bool shiftAbort, bool abortOnClick, bool purge, bool async) {
+	if (_movies.contains(movieIndex)) {
+		MMovieFile &movie = _movies.getVal(movieIndex);
+		if (segIndex <= (int)movie.segments.size() && segIndex > 0) {
+			MMovieSegment &segment = movie.segments[segIndex - 1];
+			_currentMovieIndex = movieIndex;
+			_currentSegmentIndex = segIndex;
+			_looping = looping;
+			_restore = restore;
+			_shiftAbort = shiftAbort;
+			_abortOnClick = abortOnClick;
+			_purge = purge;
+			_async = async;
+			debugC(5, kDebugXObj, "MMovieXObject::playSegment(): hitting play on movie %s (%d) segment %s (%d) - %d", movie._path.toString().c_str(), movieIndex, segment._name.c_str(), segIndex, segment._start);
+			movie._video->seek(Audio::Timestamp(0, segment._start, movie._video->getTimeScale()));
+			movie._video->start();
+
+			if (!_async) {
+				updateScreenBlocking();
+			}
+
+			return true;
+		}
+	}
+	return false;
+}
+
+bool MMovieXObject::stopSegment() {
+	if (_currentMovieIndex && _currentSegmentIndex) {
+		MMovieFile &movie = _movies.getVal(_currentMovieIndex);
+		MMovieSegment &seg = movie.segments[_currentSegmentIndex - 1];
+		debugC(5, kDebugXObj, "MMovieXObject::stopSegment(): hitting stop on movie %s (%d) segment %s (%d) - %d", movie._path.toString().c_str(), _currentMovieIndex, seg._name.c_str(), _currentSegmentIndex, seg._start);
+		if (movie._video) {
+			movie._video->stop();
+		}
+		_currentMovieIndex = 0;
+		_currentSegmentIndex = 0;
+		return true;
+	}
+	return false;
+}
+
+void MMovieXObject::updateScreenBlocking() {
+	while (_currentMovieIndex && _currentSegmentIndex) {
+		Common::Event event;
+		bool keepPlaying = true;
+		if (g_system->getEventManager()->pollEvent(event)) {
+			switch (event.type) {
+				case Common::EVENT_QUIT:
+					g_director->processEventQUIT();
+					// fallthrough
+				case Common::EVENT_KEYDOWN:
+				case Common::EVENT_RBUTTONDOWN:
+				case Common::EVENT_LBUTTONDOWN:
+					if (_abortOnClick)
+						keepPlaying = false;
+					break;
+				default:
+					break;
+			}
+		}
+		if (!keepPlaying)
+			break;
+		updateScreen();
+	}
+}
+
+void MMovieXObject::updateScreen() {
+	if (_currentMovieIndex) {
+		MMovieFile &movie = _movies.getVal(_currentMovieIndex);
+		if (_currentSegmentIndex) {
+			MMovieSegment &seg = movie.segments[_currentSegmentIndex - 1];
+			if (movie._video && movie._video->isPlaying() && movie._video->needsUpdate()) {
+				const Graphics::Surface *frame = movie._video->decodeNextFrame();
+				if (frame) {
+					debugC(5, kDebugXObj, "MMovieXObject: rendering movie %s (%d), time %d", movie._path.toString().c_str(), _currentMovieIndex, movie._video->getTime());
+					Graphics::Surface *temp1 = frame->scale(_bounds.width(), _bounds.height(), false);
+					Graphics::Surface *temp2 = temp1->convertTo(g_director->_pixelformat, movie._video->getPalette());
+					g_system->copyRectToScreen(temp2->getPixels(), temp2->pitch, _bounds.left, _bounds.top, _bounds.width(), _bounds.height());
+					delete temp2;
+					delete temp1;
+				}
+			}
+			// do a time check
+			uint32 endTime = Audio::Timestamp(0, seg._length + seg._start, movie._video->getTimeScale()).msecs();
+			debugC(5, kDebugXObj, "MMovieXObject::updateScreen(): time: %d, endTime: %d", movie._video->getTime(), endTime);
+			if (movie._video->getTime() >= endTime) {
+				if (_looping) {
+					debugC(5, kDebugXObj, "MMovieXObject::updateScreen(): rewinding loop on %s (%d), time %d", movie._path.toString().c_str(), _currentMovieIndex, movie._video->getTime());
+					movie._video->seek(Audio::Timestamp(0, seg._start, movie._video->getTimeScale()));
+				} else {
+					debugC(5, kDebugXObj, "MMovieXObject::updateScreen(): stopping %s (%d), time %d", movie._path.toString().c_str(), _currentMovieIndex, movie._video->getTime());
+					stopSegment();
+				}
+			}
+		}
+	}
+	g_system->updateScreen();
+	g_director->delayMillis(10);
+}
+
 void MMovieXObj::open(ObjectType type, const Common::Path &path) {
     MMovieXObject::initMethods(xlibMethods);
     MMovieXObject *xobj = new MMovieXObject(type);
@@ -133,21 +236,281 @@ void MMovieXObj::m_new(int nargs) {
 
 XOBJSTUB(MMovieXObj::m_Movie, 0)
 XOBJSTUBNR(MMovieXObj::m_dispose)
-XOBJSTUB(MMovieXObj::m_openMMovie, 0)
-XOBJSTUB(MMovieXObj::m_closeMMovie, 0)
-XOBJSTUB(MMovieXObj::m_playSegment, 0)
-XOBJSTUB(MMovieXObj::m_playSegLoop, 0)
-XOBJSTUB(MMovieXObj::m_idleSegment, 0)
-XOBJSTUB(MMovieXObj::m_stopSegment, 0)
-XOBJSTUB(MMovieXObj::m_seekSegment, 0)
+
+void MMovieXObj::m_openMMovie(int nargs) {
+	g_lingo->printSTUBWithArglist("MMovieXObj::m_openMMovie", nargs);
+	if (nargs != 1) {
+		g_lingo->dropStack(nargs);
+		g_lingo->push(Datum(-1));
+		return;
+	}
+	MMovieXObject *me = static_cast<MMovieXObject *>(g_lingo->_state->me.u.obj);
+	Common::String basename = g_lingo->pop().asString();
+	Common::Path path = findPath(basename);
+	if (path.empty()) {
+		g_lingo->push(MMovieError::MMOVIE_INVALID_OFFSETS_FILE);
+		return;
+	}
+	Common::Path offsetsPath = findPath(basename.substr(0, basename.size()-4) + ".ofs");
+	if (offsetsPath.empty()) {
+		g_lingo->push(MMovieError::MMOVIE_INVALID_OFFSETS_FILE);
+		return;
+	}
+	if (me->_moviePathMap.contains(basename)) {
+		g_lingo->push(MMovieError::MMOVIE_MOVIE_ALREADY_OPEN);
+		return;
+	}
+	Common::File offsetsFile;
+	if (!offsetsFile.open(offsetsPath)) {
+		g_lingo->push(MMovieError::MMOVIE_INVALID_OFFSETS_FILE);
+		return;
+	}
+
+	MMovieFile movie(path);
+	movie._video = new Video::QuickTimeDecoder();
+	if (!movie._video->loadFile(path)) {
+		warning("MMovieXObj::m_openMMovie(): unable to open QT file %s", path.toString().c_str());
+		delete movie._video;
+		movie._video = nullptr;
+	}
+	uint32 offsetCount = offsetsFile.readUint32BE();
+	offsetsFile.skip(0x3c);
+	debugC(5, kDebugXObj, "MMovieXObj:m_openMMovie(): opening movie %s (index %d)", path.toString().c_str(), me->_lastIndex);
+	for (uint32 i = 0; i < offsetCount; i++) {
+		Common::String name = offsetsFile.readString(0x20, 0x10);
+		uint32 start = offsetsFile.readUint32BE();
+		uint32 length = offsetsFile.readUint32BE();
+		debugC(5, kDebugXObj, "MMovieXObj:m_openMMovie(): adding segment %s (index %d): start %d (%dms) length %d (%dms)", name.c_str(), movie.segments.size(), start, Audio::Timestamp(0, start, movie._video->getTimeScale()).msecs(), length, Audio::Timestamp(0, length, movie._video->getTimeScale()).msecs());
+		movie.segments.push_back(MMovieSegment(name, start, length));
+		movie.segLookup.setVal(name, movie.segments.size());
+	}
+	me->_movies.setVal(me->_lastIndex, movie);
+	me->_moviePathMap.setVal(basename, me->_lastIndex);
+	g_lingo->push(me->_lastIndex);
+	me->_lastIndex += 1;
+}
+
+void MMovieXObj::m_closeMMovie(int nargs) {
+	g_lingo->printSTUBWithArglist("MMovieXObj::m_closeMMovie", nargs);
+	if (nargs != 1) {
+		g_lingo->dropStack(nargs);
+		g_lingo->push(Datum(MMovieError::MMOVIE_INVALID_MOVIE_INDEX));
+		return;
+	}
+	MMovieXObject *me = static_cast<MMovieXObject *>(g_lingo->_state->me.u.obj);
+	int index = g_lingo->pop().asInt();
+	if (!me->_movies.contains(index)) {
+		warning("MMovieXObj::m_closeMMovie(): movie index %d not found", index);
+		g_lingo->push(Datum(MMovieError::MMOVIE_INVALID_MOVIE_INDEX));
+		return;
+	}
+	for (auto &it : me->_moviePathMap) {
+		if (it._value == index) {
+			me->_moviePathMap.erase(it._key);
+			break;
+		}
+	}
+	MMovieFile &file = me->_movies.getVal(index);
+	debugC(5, kDebugXObj, "MMovieXObj:m_openMMovie(): closing movie %s (index %d)", file._path.toString().c_str(), me->_lastIndex);
+	if (file._video) {
+		delete file._video;
+		file._video = nullptr;
+	}
+	me->_movies.erase(index);
+	g_lingo->push(0);
+}
+
+void MMovieXObj::m_playSegment(int nargs) {
+	g_lingo->printSTUBWithArglist("MMovieXObj::m_playSegment", nargs);
+	if (nargs != 5) {
+		g_lingo->dropStack(nargs);
+		g_lingo->push(Datum(MMovieError::MMOVIE_INVALID_SEGMENT_NAME));
+		return;
+	}
+
+	Common::String asyncOpt = g_lingo->pop().asString();
+	Common::String purgeOpt = g_lingo->pop().asString();
+	Common::String abortOpt = g_lingo->pop().asString();
+	Common::String restoreOpt = g_lingo->pop().asString();
+	Common::String segmentName = g_lingo->pop().asString();
+
+	bool restore = restoreOpt.equalsIgnoreCase("restore");
+	bool shiftAbort = abortOpt.equalsIgnoreCase("shiftAbort");
+	bool abortOnClick = abortOpt.equalsIgnoreCase("abortOnClick");
+	bool purge = purgeOpt.equalsIgnoreCase("purge");
+	bool async = asyncOpt.equalsIgnoreCase("async");
+
+	MMovieXObject *me = static_cast<MMovieXObject *>(g_lingo->_state->me.u.obj);
+	for (auto &it : me->_movies) {
+		if (it._value.segLookup.contains(segmentName)) {
+			int segIndex = it._value.segLookup.getVal(segmentName);
+			if (!me->playSegment(it._key, segIndex, false, restore, shiftAbort, abortOnClick, purge, async)) {
+				g_lingo->push(MMovieError::MMOVIE_INDEX_OUT_OF_RANGE);
+				return;
+			}
+			g_lingo->push(0);
+			return;
+		}
+	}
+	g_lingo->push(Datum(MMovieError::MMOVIE_INVALID_SEGMENT_NAME));
+	return;
+}
+
+void MMovieXObj::m_playSegLoop(int nargs) {
+	g_lingo->printSTUBWithArglist("MMovieXObj::m_playSegLoop", nargs);
+	if (nargs != 5) {
+		g_lingo->dropStack(nargs);
+		g_lingo->push(Datum(MMovieError::MMOVIE_INVALID_SEGMENT_NAME));
+		return;
+	}
+
+	Common::String asyncOpt = g_lingo->pop().asString();
+	Common::String purgeOpt = g_lingo->pop().asString();
+	Common::String abortOpt = g_lingo->pop().asString();
+	Common::String restoreOpt = g_lingo->pop().asString();
+	Common::String segmentName = g_lingo->pop().asString();
+
+	bool restore = restoreOpt.equalsIgnoreCase("restore");
+	bool shiftAbort = abortOpt.equalsIgnoreCase("shiftAbort");
+	bool abortOnClick = abortOpt.equalsIgnoreCase("abortOnClick");
+	bool purge = abortOpt.equalsIgnoreCase("purge");
+	bool async = asyncOpt.equalsIgnoreCase("async");
+
+	MMovieXObject *me = static_cast<MMovieXObject *>(g_lingo->_state->me.u.obj);
+	for (auto &it : me->_movies) {
+		if (it._value.segLookup.contains(segmentName)) {
+			int segIndex = it._value.segLookup.getVal(segmentName);
+			me->playSegment(it._key, segIndex, true, restore, shiftAbort, abortOnClick, purge, async);
+			g_lingo->push(0);
+			return;
+		}
+	}
+	g_lingo->push(Datum(MMovieError::MMOVIE_INVALID_SEGMENT_NAME));
+	return;
+}
+
+void MMovieXObj::m_idleSegment(int nargs) {
+	debugC(5, kDebugXObj, "MMovieXObj::m_idleSegment()");
+	if (nargs != 0) {
+		g_lingo->dropStack(nargs);
+	}
+	MMovieXObject *me = static_cast<MMovieXObject *>(g_lingo->_state->me.u.obj);
+	me->updateScreen();
+	g_lingo->push(0);
+}
+
+void MMovieXObj::m_stopSegment(int nargs) {
+	g_lingo->printSTUBWithArglist("MMovieXObj::m_stopSegment", nargs);
+	if (nargs != 0) {
+		g_lingo->dropStack(nargs);
+	}
+	MMovieXObject *me = static_cast<MMovieXObject *>(g_lingo->_state->me.u.obj);
+	me->stopSegment();
+	g_lingo->push(0);
+}
+
+void MMovieXObj::m_seekSegment(int nargs) {
+	g_lingo->printSTUBWithArglist("MMovieXObj::m_seekSegment", nargs);
+	if (nargs != 1) {
+		g_lingo->dropStack(nargs);
+		g_lingo->push(MMovieError::MMOVIE_INVALID_SEGMENT_NAME);
+		return;
+	}
+	Common::String segmentName = g_lingo->pop().asString();
+	MMovieXObject *me = static_cast<MMovieXObject *>(g_lingo->_state->me.u.obj);
+	for (auto &it : me->_movies) {
+		if (it._value.segLookup.contains(segmentName)) {
+
+		}
+	}
+
+}
+
+
 XOBJSTUB(MMovieXObj::m_setSegmentTime, 0)
-XOBJSTUB(MMovieXObj::m_setDisplayBounds, 0)
+
+void MMovieXObj::m_setDisplayBounds(int nargs) {
+	g_lingo->printSTUBWithArglist("MMovieXObj::m_setDisplayBounds", nargs);
+	if (nargs != 4) {
+		warning("MMovieXObj::m_setDisplayBounds: expecting 4 arguments!");
+		g_lingo->dropStack(nargs);
+		g_lingo->push(Datum(0));
+		return;
+	}
+	MMovieXObject *me = static_cast<MMovieXObject *>(g_lingo->_state->me.u.obj);
+
+	Datum bottom = g_lingo->pop();
+	Datum right = g_lingo->pop();
+	Datum top = g_lingo->pop();
+	Datum left = g_lingo->pop();
+	me->_bounds = Common::Rect((int16)left.asInt(), (int16)top.asInt(), (int16)right.asInt(), (int16)bottom.asInt());
+	g_lingo->push(Datum(0));
+}
+
 XOBJSTUB(MMovieXObj::m_getMovieNormalWidth, 0)
 XOBJSTUB(MMovieXObj::m_getMovieNormalHeight, 0)
-XOBJSTUB(MMovieXObj::m_getSegCount, 0)
-XOBJSTUB(MMovieXObj::m_getSegName, "")
-XOBJSTUB(MMovieXObj::m_getMovieRate, 0)
-XOBJSTUB(MMovieXObj::m_setMovieRate, 0)
+
+void MMovieXObj::m_getSegCount(int nargs) {
+	g_lingo->printSTUBWithArglist("MMovieXObj::m_getSegCount", nargs);
+	if (nargs != 1) {
+		g_lingo->dropStack(nargs);
+		g_lingo->push(MMovieError::MMOVIE_INVALID_MOVIE_INDEX);
+		return;
+	}
+	MMovieXObject *me = static_cast<MMovieXObject *>(g_lingo->_state->me.u.obj);
+	int movieIndex = g_lingo->pop().asInt();
+	if (!me->_movies.contains(movieIndex)) {
+		g_lingo->push(MMovieError::MMOVIE_INVALID_MOVIE_INDEX);
+		return;
+	}
+	g_lingo->push((int)me->_movies.getVal(movieIndex).segments.size());
+}
+
+void MMovieXObj::m_getSegName(int nargs) {
+	if (nargs != 2) {
+		g_lingo->dropStack(nargs);
+		g_lingo->push(Common::String(""));
+		return;
+	}
+	MMovieXObject *me = static_cast<MMovieXObject *>(g_lingo->_state->me.u.obj);
+	int segmentIndex = g_lingo->pop().asInt();
+	int movieIndex = g_lingo->pop().asInt();
+	if (!me->_movies.contains(movieIndex)) {
+		g_lingo->push(Common::String(""));
+		return;
+	}
+	if (segmentIndex > (int)me->_movies.getVal(movieIndex).segments.size() ||
+			segmentIndex <= 0) {
+		g_lingo->push(Common::String(""));
+		return;
+	}
+	Common::String result = me->_movies.getVal(movieIndex).segments[segmentIndex - 1]._name;
+	debugC(5, kDebugXObj, "MMovieXObj::m_getSegName(%d, %d): %s", movieIndex, segmentIndex, result.c_str());
+	g_lingo->push(result);
+}
+
+void MMovieXObj::m_getMovieRate(int nargs) {
+	g_lingo->printSTUBWithArglist("MMovieXObj::m_getMovieRate", nargs);
+	if (nargs != 0) {
+		g_lingo->dropStack(nargs);
+	}
+	MMovieXObject *me = static_cast<MMovieXObject *>(g_lingo->_state->me.u.obj);
+	g_lingo->push(Datum(me->_rate));
+}
+
+void MMovieXObj::m_setMovieRate(int nargs) {
+	g_lingo->printSTUBWithArglist("MMovieXObj::m_setMovieRate", nargs);
+	if (nargs != 1) {
+		warning("MMovieXObj::m_setMovieRate: expecting 4 arguments!");
+		g_lingo->dropStack(nargs);
+		g_lingo->push(Datum(0));
+		return;
+	}
+	MMovieXObject *me = static_cast<MMovieXObject *>(g_lingo->_state->me.u.obj);
+	me->_rate = g_lingo->pop().asInt();
+	g_lingo->push(Datum(me->_rate));
+}
+
 XOBJSTUB(MMovieXObj::m_flushEvents, 0)
 XOBJSTUB(MMovieXObj::m_invalidateRect, 0)
 XOBJSTUB(MMovieXObj::m_readFile, "")
diff --git a/engines/director/lingo/xlibs/mmovie.h b/engines/director/lingo/xlibs/mmovie.h
index b38e071bef2..ac5a8d9fed5 100644
--- a/engines/director/lingo/xlibs/mmovie.h
+++ b/engines/director/lingo/xlibs/mmovie.h
@@ -22,11 +22,67 @@
 #ifndef DIRECTOR_LINGO_XLIBS_MMOVIE_H
 #define DIRECTOR_LINGO_XLIBS_MMOVIE_H
 
+#include "common/hash-str.h"
+#include "video/qt_decoder.h"
 namespace Director {
 
+// taken from shared:1124:mHandleError
+enum MMovieError {
+	MMOVIE_NO_STAGE = -1,
+	MMOVIE_TOO_MANY_OPEN_FILES = -2,
+	MMOVIE_MOVIE_ALREADY_OPEN = -3,
+	MMOVIE_INVALID_MOVIE_INDEX = -4,
+	MMOVIE_INVALID_OFFSETS_FILE = -5,
+	MMOVIE_INVALID_SEGMENT_OFFSET = -6,
+	MMOVIE_NO_MOVIES_OPEN = -7,
+	MMOVIE_INVALID_SEGMENT_NAME = -8,
+	MMOVIE_INDEX_OUT_OF_RANGE = -9,
+	MMOVIE_CONTINUE_WITHOUT_PLAYING = -10,
+	MMOVIE_ABORT_DOUBLE_CLICK = -11,
+};
+
+struct MMovieSegment {
+	Common::String _name;
+	uint32 _start = 0;
+	uint32 _length = 0;
+	MMovieSegment() {}
+	MMovieSegment(Common::String name, uint32 start, uint32 length) : _name(name), _start(start), _length(length) {}
+};
+
+struct MMovieFile {
+	int _lastIndex = 0;
+	Common::Path _path;
+	Common::Array<MMovieSegment> segments;
+	Common::HashMap<Common::String, uint32, Common::IgnoreCase_Hash, Common::IgnoreCase_EqualTo> segLookup;
+	Video::QuickTimeDecoder *_video = nullptr;
+	MMovieFile() {}
+	MMovieFile(Common::Path path) : _path(path) {}
+};
+
 class MMovieXObject : public Object<MMovieXObject> {
 public:
 	MMovieXObject(ObjectType objType);
+
+	int _rate = 100;
+	Common::Rect _bounds;
+	int _lastIndex = 1;
+	int _currentMovieIndex = 0;
+	int _currentSegmentIndex = 0;
+	bool _looping = false;
+	bool _restore = false;
+	bool _shiftAbort = false;
+	bool _abortOnClick = false;
+	bool _purge = false;
+	bool _async = false;
+
+	Common::HashMap<int, MMovieFile> _movies;
+	Common::HashMap<Common::String, int, Common::IgnoreCase_Hash, Common::IgnoreCase_EqualTo> _moviePathMap;
+
+
+	bool playSegment(int movieIndex, int segIndex, bool looping, bool restore, bool shiftAbort, bool abortOnClick, bool purge, bool async);
+	bool stopSegment();
+	void updateScreenBlocking();
+	void updateScreen();
 };
 
 namespace MMovieXObj {


Commit: ec56a7c2fec96a55386d53333bbd2e2b5b12435a
    https://github.com/scummvm/scummvm/commit/ec56a7c2fec96a55386d53333bbd2e2b5b12435a
Author: Scott Percival (code at moral.net.au)
Date: 2024-05-12T22:00:19+02:00

Commit Message:
DIRECTOR: Fix kTheCursor output after Cursor::readFromCast

Changed paths:
    engines/director/cursor.cpp


diff --git a/engines/director/cursor.cpp b/engines/director/cursor.cpp
index 853acceb37c..586489816e8 100644
--- a/engines/director/cursor.cpp
+++ b/engines/director/cursor.cpp
@@ -82,7 +82,7 @@ void Cursor::readFromCast(Datum cursorCasts) {
 	_usePalette = false;
 	_keyColor = 3;
 
-	resetCursor(Graphics::kMacCursorCustom, true, cursorCasts);
+	resetCursor(Graphics::kMacCursorCustom, true, Datum(cursorId.member));
 
 	BitmapCastMember *cursorBitmap = (BitmapCastMember *)cursorCast;
 	BitmapCastMember *maskBitmap = (BitmapCastMember *)maskCast;


Commit: f8210d9b58d8299087d58f2bf3d6f602abfcd9f4
    https://github.com/scummvm/scummvm/commit/f8210d9b58d8299087d58f2bf3d6f602abfcd9f4
Author: Scott Percival (code at moral.net.au)
Date: 2024-05-12T22:00:19+02:00

Commit Message:
DIRECTOR: Fix cast member name cache

In Director you can have multiple cast members with the same name.
Looking up a cast member by name should always return the first member
ID; we don't necessarily load the cast members in order, so we need
to verify this. Also any cast manipulation actions should update or
refresh the name cache.

Fixes the bitmaps chosen for the welcome cube at the start of Virtual Nightclub.

Changed paths:
    engines/director/cast.cpp
    engines/director/cast.h
    engines/director/castmember/castmember.cpp


diff --git a/engines/director/cast.cpp b/engines/director/cast.cpp
index 54c607f8fa5..7a48f6e2fb6 100644
--- a/engines/director/cast.cpp
+++ b/engines/director/cast.cpp
@@ -636,6 +636,10 @@ void Cast::loadCast() {
 		}
 	}
 
+	// The CastMemberInfo data should be loaded by now,
+	// set up the cache used for cast member name lookups.
+	rebuildCastNameCache();
+
 	// For D4+ we may request to force Lingo scripts and skip precompiled bytecode
 	if (_version >= kFileVer400 && !debugChannelSet(-1, kDebugNoBytecode)) {
 		// Try to load script context
@@ -1301,21 +1305,6 @@ void Cast::loadCastInfo(Common::SeekableReadStreamEndian &stream, uint16 id) {
 		// fallthrough
 	case 2:
 		ci->name = castInfo.strings[1].readString();
-
-		if (!ci->name.empty()) {
-			// Multiple casts can have the same name. In director only the first one is used.
-			if (!_castsNames.contains(ci->name)) {
-				_castsNames[ci->name] = id;
-			}
-
-			// Store name with type
-			Common::String cname = Common::String::format("%s:%d", ci->name.c_str(), member->_type);
-			if (!_castsNames.contains(cname)) {
-				_castsNames[cname] = id;
-			} else {
-				debugC(4, kDebugLoading, "Cast::loadCastInfo(): duplicate cast name: %s for castIDs: %s %s", cname.c_str(), numToCastNum(id), numToCastNum(_castsNames[ci->name]));
-			}
-		}
 		// fallthrough
 	case 1:
 		ci->script = castInfo.strings[0].readString(false);
@@ -1511,4 +1500,25 @@ Common::String Cast::formatCastSummary(int castId = -1) {
 	return result;
 }
 
+void Cast::rebuildCastNameCache() {
+	_castsNames.clear();
+	for (auto &it : _castsInfo) {
+		if (!it._value->name.empty()) {
+			// Multiple casts can have the same name. In director only the earliest one is used for lookups.
+			if (!_castsNames.contains(it._value->name) || (_castsNames.getVal(it._value->name) > it._key)) {
+				_castsNames[it._value->name] = it._key;
+			}
+
+			// Store name with type
+			CastMember *member = _loadedCast->getVal(it._key);
+			Common::String cname = Common::String::format("%s:%d", it._value->name.c_str(), member->_type);
+			if (!_castsNames.contains(cname) || (_castsNames.getVal(cname) > it._key)) {
+				_castsNames[cname] = it._key;
+			} else {
+				debugC(4, kDebugLoading, "Cast::rebuildCastNameCache(): duplicate cast name: %s for castIDs: %s %s", cname.c_str(), numToCastNum(it._key), numToCastNum(_castsNames[it._value->name]));
+			}
+		}
+	}
+}
+
 } // End of namespace Director
diff --git a/engines/director/cast.h b/engines/director/cast.h
index 7933ce4ceb8..ac693e520c1 100644
--- a/engines/director/cast.h
+++ b/engines/director/cast.h
@@ -112,6 +112,7 @@ public:
 	const Stxt *getStxt(int castId);
 	Common::String getVideoPath(int castId);
 	Common::SeekableReadStreamEndian *getResource(uint32 tag, uint16 id);
+	void rebuildCastNameCache();
 
 	// release all castmember's widget, should be called when we are changing movie.
 	// because widget is handled by channel, thus we should clear all of those run-time info when we are switching the movie. (because we will create new widgets for cast)
diff --git a/engines/director/castmember/castmember.cpp b/engines/director/castmember/castmember.cpp
index 7e199ca608c..93bfb301f1e 100644
--- a/engines/director/castmember/castmember.cpp
+++ b/engines/director/castmember/castmember.cpp
@@ -228,6 +228,7 @@ bool CastMember::setField(int field, const Datum &d) {
 			return false;
 		}
 		castInfo->name = d.asString();
+		_cast->rebuildCastNameCache();
 		return true;
 	case kTheRect:
 		warning("CastMember::setField(): Attempt to set read-only field \"%s\" of cast %d", g_lingo->field2str(field), _castId);


Commit: 9ac91fc68c02227f48a9b15b30b107d36c859456
    https://github.com/scummvm/scummvm/commit/9ac91fc68c02227f48a9b15b30b107d36c859456
Author: Scott Percival (code at moral.net.au)
Date: 2024-05-12T22:00:19+02:00

Commit Message:
DIRECTOR: XOBJ: Fix UnitTest calling convention to match DLL

Changed paths:
    engines/director/lingo/lingo-object.cpp
    engines/director/lingo/xlibs/unittest.cpp
    engines/director/lingo/xlibs/unittest.h


diff --git a/engines/director/lingo/lingo-object.cpp b/engines/director/lingo/lingo-object.cpp
index 838223e23a8..6cc942dca25 100644
--- a/engines/director/lingo/lingo-object.cpp
+++ b/engines/director/lingo/lingo-object.cpp
@@ -276,7 +276,7 @@ static struct XLibProto {
 	{ SoundJam::fileNames,				SoundJam::open,				SoundJam::close,			kXObj,					400 },	// D4
 	{ SpaceMgr::fileNames,				SpaceMgr::open,				SpaceMgr::close,			kXObj,					400 },	// D4
 	{ StageTCXObj::fileNames,			StageTCXObj::open,			StageTCXObj::close,			kXObj,					400 },	// D4
-	{ UnitTest::fileNames,				UnitTest::open,				UnitTest::close,			kXObj,					400 },	// D4
+	{ UnitTestXObj::fileNames,			UnitTestXObj::open,			UnitTestXObj::close,		kXObj,					400 },	// D4
 	{ VMisOnXFCN::fileNames,			VMisOnXFCN::open,			VMisOnXFCN::close,			kXObj,					400 },	// D4
 	{ ValkyrieXObj::fileNames,			ValkyrieXObj::open,			ValkyrieXObj::close,		kXObj,					400 },	// D4
 	{ VideodiscXObj::fileNames,			VideodiscXObj::open,		VideodiscXObj::close,		kXObj,					200 },	// D2
diff --git a/engines/director/lingo/xlibs/unittest.cpp b/engines/director/lingo/xlibs/unittest.cpp
index 59588035efb..766494267b0 100644
--- a/engines/director/lingo/xlibs/unittest.cpp
+++ b/engines/director/lingo/xlibs/unittest.cpp
@@ -42,39 +42,56 @@
 
 namespace Director {
 
-const char *UnitTest::xlibName = "UnitTest";
-const char *UnitTest::fileNames[] = {
+const char *UnitTestXObj::xlibName = "UnitTest";
+const char *UnitTestXObj::fileNames[] = {
 	"UnitTest",
 	0
 };
 
-static BuiltinProto builtins[] = {
-	{ "UTScreenshot", UnitTest::m_UTScreenshot, 0, 1, 400, HBLTIN },
-	{ nullptr, nullptr, 0, 0, 0, VOIDSYM }
+static MethodProto xlibMethods[] = {
+	{ "new",				UnitTestXObj::m_new,				 0, 0,	400 },	// D4
+	{ "screenshot",			UnitTestXObj::m_screenshot,			 1, 1,  400 },	// D4
+	{ nullptr, nullptr, 0, 0, 0 }
 };
 
-void UnitTest::open(ObjectType type, const Common::Path &path) {
-	g_lingo->initBuiltIns(builtins);
+void UnitTestXObj::open(ObjectType type, const Common::Path &path) {
+	if (type == kXObj) {
+		UnitTestXObject::initMethods(xlibMethods);
+		UnitTestXObject *xobj = new UnitTestXObject(kXObj);
+		g_lingo->exposeXObject(xlibName, xobj);
+	}
+}
+
+void UnitTestXObj::close(ObjectType type) {
+	if (type == kXObj) {
+		UnitTestXObject::cleanupMethods();
+		g_lingo->_globalvars[xlibName] = Datum();
+	}
+}
+
+UnitTestXObject::UnitTestXObject(ObjectType ObjectType) :Object<UnitTestXObject>("UnitTest") {
+	_objType = ObjectType;
 }
 
-void UnitTest::close(ObjectType type) {
-	g_lingo->cleanupBuiltIns(builtins);
+void UnitTestXObj::m_new(int nargs) {
+	g_lingo->push(g_lingo->_state->me);
 }
 
-void UnitTest::m_UTScreenshot(int nargs) {
+void UnitTestXObj::m_screenshot(int nargs) {
 	Common::String filenameBase = g_director->getCurrentMovie()->getArchive()->getFileName();
 	if (filenameBase.hasSuffixIgnoreCase(".dir"))
 		filenameBase = filenameBase.substr(0, filenameBase.size() - 4);
 
 	if (nargs > 1) {
 		g_lingo->dropStack(nargs - 1);
+		nargs = 1;
 	}
 	if (nargs == 1) {
 		Datum name = g_lingo->pop();
 		if (name.type == STRING) {
 			filenameBase = *name.u.s;
 		} else if (name.type != VOID) {
-			warning("UnitTest::b_UTScreenshot(): expected string for arg 1, ignoring");
+			warning("UnitTestXObj::m_screenshot(): expected string for arg 1, ignoring");
 		}
 	}
 
@@ -99,7 +116,7 @@ void UnitTest::m_UTScreenshot(int nargs) {
 
 	Common::SeekableWriteStream *stream = file.createWriteStream();
 	if (!stream) {
-		warning("UnitTest::b_UTScreenshot(): could not open file %s", file.getPath().toString(Common::Path::kNativeSeparator).c_str());
+		warning("UnitTestXObj::m_screenshot(): could not open file %s", file.getPath().toString(Common::Path::kNativeSeparator).c_str());
 		return;
 	}
 
@@ -114,7 +131,7 @@ void UnitTest::m_UTScreenshot(int nargs) {
 	success = Image::writeBMP(*stream, *windowSurface);
 #endif
 	if (!success) {
-		warning("UnitTest::b_UTScreenshot(): error writing screenshot data to file %s", file.getPath().toString(Common::Path::kNativeSeparator).c_str());
+		warning("UnitTestXObj::m_screenshot(): error writing screenshot data to file %s", file.getPath().toString(Common::Path::kNativeSeparator).c_str());
 	}
 	stream->finalize();
 	delete stream;
diff --git a/engines/director/lingo/xlibs/unittest.h b/engines/director/lingo/xlibs/unittest.h
index 65041d7c4b3..903d9be332d 100644
--- a/engines/director/lingo/xlibs/unittest.h
+++ b/engines/director/lingo/xlibs/unittest.h
@@ -24,7 +24,12 @@
 
 namespace Director {
 
-namespace UnitTest {
+class UnitTestXObject : public Object<UnitTestXObject> {
+public:
+	UnitTestXObject(ObjectType objType);
+};
+
+namespace UnitTestXObj {
 
 extern const char *xlibName;
 extern const char *fileNames[];
@@ -32,7 +37,8 @@ extern const char *fileNames[];
 void open(ObjectType type, const Common::Path &path);
 void close(ObjectType type);
 
-void m_UTScreenshot(int nargs);
+void m_new(int nargs);
+void m_screenshot(int nargs);
 
 } // End of namespace UnitTest
 


Commit: 45c9abff9381d549909ed7205f201b5bd0c0ea74
    https://github.com/scummvm/scummvm/commit/45c9abff9381d549909ed7205f201b5bd0c0ea74
Author: Scott Percival (code at moral.net.au)
Date: 2024-05-12T22:00:19+02:00

Commit Message:
DIRECTOR: Refactor Channel::getMask()

The Mask ink type allows Director to combine a sprite with a 1bpp
mask bitmap to stencil an image. Previously we were using the widget
drawing code to generate this image, which caused issues with scaling
and depth (as the surface was formatted to match the screen). The new
code checks for a 1bpp image specifically and sends it through, removing
the workarounds for screen depth.

Fixes the drawing of the welcome cube at the start of Virtual Nightclub.

Changed paths:
    engines/director/castmember/bitmap.cpp
    engines/director/castmember/bitmap.h
    engines/director/channel.cpp
    engines/director/channel.h
    engines/director/graphics.cpp
    engines/director/window.cpp


diff --git a/engines/director/castmember/bitmap.cpp b/engines/director/castmember/bitmap.cpp
index 779c236f9c8..1f11af9831b 100644
--- a/engines/director/castmember/bitmap.cpp
+++ b/engines/director/castmember/bitmap.cpp
@@ -215,8 +215,10 @@ BitmapCastMember::~BitmapCastMember() {
 		delete _ditheredImg;
 	}
 
-	if (_matte)
+	if (_matte) {
+		_matte->free();
 		delete _matte;
+	}
 }
 
 Graphics::MacWidget *BitmapCastMember::createWidget(Common::Rect &bbox, Channel *channel, SpriteType spriteType) {
@@ -466,21 +468,34 @@ void BitmapCastMember::createMatte(Common::Rect &bbox) {
 	if (!colorFound) {
 		debugC(1, kDebugImages, "BitmapCastMember::createMatte(): No white color for matte image");
 	} else {
-		delete _matte;
+		if (_matte) {
+			_matte->free();
+			delete _matte;
+		}
 
-		_matte = new Graphics::FloodFill(&tmp, whiteColor, 0, true);
+		Graphics::FloodFill matteFill(&tmp, whiteColor, 0, true);
 
 		for (int yy = 0; yy < tmp.h; yy++) {
-			_matte->addSeed(0, yy);
-			_matte->addSeed(tmp.w - 1, yy);
+			matteFill.addSeed(0, yy);
+			matteFill.addSeed(tmp.w - 1, yy);
 		}
 
 		for (int xx = 0; xx < tmp.w; xx++) {
-			_matte->addSeed(xx, 0);
-			_matte->addSeed(xx, tmp.h - 1);
+			matteFill.addSeed(xx, 0);
+			matteFill.addSeed(xx, tmp.h - 1);
 		}
 
-		_matte->fillMask();
+		matteFill.fillMask();
+		Graphics::Surface *matteSurf = matteFill.getMask();
+		// convert the mask to the same surface format used for 1bpp bitmaps.
+		// this uses the director palette scheme, so white is 0x00 and black is 0xff.
+		_matte = new Graphics::Surface();
+		_matte->create(matteSurf->w, matteSurf->h, Graphics::PixelFormat::createFormatCLUT8());
+		for (int y = 0; y < matteSurf->h; y++) {
+			for (int x = 0; x < matteSurf->w; x++) {
+				_matte->setPixel(x, y, matteSurf->getPixel(x, y) ? 0x00 : 0xff);
+			}
+		}
 		_noMatte = false;
 	}
 
@@ -494,12 +509,11 @@ Graphics::Surface *BitmapCastMember::getMatte(Common::Rect &bbox) {
 	}
 
 	// check for the scale matte
-	Graphics::Surface *surface = _matte ? _matte->getMask() : nullptr;
-	if (surface && (surface->w != bbox.width() || surface->h != bbox.height())) {
+	if (_matte && (_matte->w != bbox.width() || _matte->h != bbox.height())) {
 		createMatte(bbox);
 	}
 
-	return _matte ? _matte->getMask() : nullptr;
+	return _matte;
 }
 
 Common::String BitmapCastMember::formatInfo() {
diff --git a/engines/director/castmember/bitmap.h b/engines/director/castmember/bitmap.h
index 09139cf4b93..08e94a6443a 100644
--- a/engines/director/castmember/bitmap.h
+++ b/engines/director/castmember/bitmap.h
@@ -62,7 +62,7 @@ public:
 
 	Picture *_picture = nullptr;
 	Graphics::Surface *_ditheredImg;
-	Graphics::FloodFill *_matte;
+	Graphics::Surface *_matte;
 
 	uint16 _pitch;
 	uint16 _regX;
diff --git a/engines/director/channel.cpp b/engines/director/channel.cpp
index f24b985ee0d..ad7ac0a4dd4 100644
--- a/engines/director/channel.cpp
+++ b/engines/director/channel.cpp
@@ -24,7 +24,9 @@
 #include "director/score.h"
 #include "director/cast.h"
 #include "director/channel.h"
+#include "director/picture.h"
 #include "director/sprite.h"
+#include "director/types.h"
 #include "director/window.h"
 #include "director/castmember/castmember.h"
 #include "director/castmember/bitmap.h"
@@ -186,16 +188,48 @@ const Graphics::Surface *Channel::getMask(bool forceMatte) {
 		CastMemberID maskID(_sprite->_castId.member + 1, _sprite->_castId.castLib);
 		CastMember *member = g_director->getCurrentMovie()->getCastMember(maskID);
 
-		if (member && member->_initialRect == _sprite->_cast->_initialRect) {
-			Graphics::MacWidget *widget = member->createWidget(bbox, this, _sprite->_spriteType);
-			if (_mask)
+		if (member) {
+			if (member->_type != kCastBitmap) {
+				warning("Channel::getMask(): Requested cast mask %s, but type is %s, not bitmap", maskID.asString().c_str(), castType2str(member->_type));
+				return nullptr;
+			}
+			BitmapCastMember *bitmap = (BitmapCastMember *)member;
+			if (bitmap->_bitsPerPixel != 1) {
+				warning("Channel::getMask(): Requested cast mask %s, but bitmap isn't 1bpp", maskID.asString().c_str());
+				return nullptr;
+			}
+
+			if (_mask) {
 				delete _mask;
-			_mask = new Graphics::ManagedSurface();
-			_mask->copyFrom(*widget->getSurface());
-			delete widget;
-			return &_mask->rawSurface();
+				_mask = nullptr;
+			}
+			if (bitmap->_picture) {
+				// reposition channel bounding box, so origin is at registration offset
+				Common::Point originPos = getPosition();
+				bbox.translate(-originPos.x, -originPos.y);
+				// create new mask surface, with the exact dimensions of the channel.
+				_mask = new Graphics::ManagedSurface(bbox.width(), bbox.height());
+				// get the bounding box of the mask image (origin at registration offset)
+				Common::Rect destRect = bitmap->getBbox();
+				// get position of channel's registration offset (origin at top left)
+				Common::Point channelRegOffset(-bbox.left, -bbox.top);
+				// move destination rect to sit at the channel's registration offset
+				destRect.translate(channelRegOffset.x, channelRegOffset.y);
+				Common::Point destOrigin(destRect.left, destRect.top);
+				// clip the destination rect so it is contained within the mask bounds
+				destRect.clip(_mask->getBounds());
+				// make a copy of the destination rect with the origin at the top left of the mask bitmap
+				Common::Rect srcRect = destRect;
+				srcRect.translate(-destOrigin.x, -destOrigin.y);
+				debugC(8, kDebugImages, "Channel::getMask(): cast mask %s, orig %dx%d, dest %dx%d, crop %d,%d %dx%d",  maskID.asString().c_str(), bitmap->_picture->_surface.w, bitmap->_picture->_surface.h, bbox.width(), bbox.height(), destRect.left, destRect.top, destRect.width(), destRect.height());
+				_mask->copyRectToSurface(bitmap->_picture->_surface, destRect.left, destRect.top, srcRect);
+				return &_mask->rawSurface();
+			} else {
+				warning("Channel::getMask(): Requested cast mask %s, but no picture found", maskID.asString().c_str());
+				return nullptr;
+			}
 		} else {
-			warning("Channel::getMask(): Requested cast mask, but no matching mask was found");
+			warning("Channel::getMask(): Requested cast mask %s, but was not found", maskID.asString().c_str());
 			return nullptr;
 		}
 	}
@@ -267,7 +301,7 @@ bool Channel::isMouseIn(const Common::Point &pos) {
 	if (_sprite->_ink == kInkTypeMatte) {
 		if (_sprite->_cast && _sprite->_cast->_type == kCastBitmap) {
 			Graphics::Surface *matte = ((BitmapCastMember *)_sprite->_cast)->getMatte(bbox);
-			return matte ? !(*(byte *)(matte->getBasePtr(pos.x - bbox.left, pos.y - bbox.top))) : true;
+			return matte ? *(byte *)(matte->getBasePtr(pos.x - bbox.left, pos.y - bbox.top)) : true;
 		}
 	}
 
@@ -295,7 +329,7 @@ bool Channel::isMatteIntersect(Channel *channel) {
 			const byte *your = (const byte *)yourMatte->getBasePtr(intersectRect.left - yourBbox.left, i - yourBbox.top);
 
 			for (int j = intersectRect.left; j < intersectRect.right; j++, my++, your++)
-				if (!*my && !*your)
+				if (*my && *your)
 					return true;
 		}
 	}
@@ -325,7 +359,7 @@ bool Channel::isMatteWithin(Channel *channel) {
 			const byte *your = (const byte *)yourMatte->getBasePtr(intersectRect.left - yourBbox.left, i - yourBbox.top);
 
 			for (int j = intersectRect.left; j < intersectRect.right; j++, my++, your++)
-				if (*my && !*your)
+				if (!*my && *your)
 					return false;
 		}
 
@@ -354,15 +388,19 @@ bool Channel::isVideoDirectToStage() {
 	return ((DigitalVideoCastMember *)_sprite->_cast)->_directToStage;
 }
 
-Common::Rect Channel::getBbox(bool unstretched) {
+bool Channel::isBboxDeterminedByChannel() {
 	bool isShape = _sprite->_cast && _sprite->_cast->_type == kCastShape;
 	// Use the dimensions and position in the Channel:
 	// - if the sprite is of a shape, or
 	// - if the sprite has the puppet flag enabled
 	// Otherwise, use the Sprite dimensions and position (i.e. taken from the
 	// frame data in the Score).
+	return (isShape || _sprite->_puppet || _sprite->_moveable);
+}
+
+Common::Rect Channel::getBbox(bool unstretched) {
 	// Setting unstretched to true always returns the Sprite dimensions.
-	bool useOverride = (isShape || _sprite->_puppet || _sprite->_moveable) && !unstretched;
+	bool useOverride = isBboxDeterminedByChannel() && !unstretched;
 
 	Common::Rect result(
 		useOverride ? _width : _sprite->_width,
@@ -376,11 +414,18 @@ Common::Rect Channel::getBbox(bool unstretched) {
 	// The origin of the rect should be at the registration offset,
 	// e.g. for bitmap sprites this defaults to the centre.
 	// Now we move the rect to the correct spot.
-	result.translate(
+	Common::Point startPos = getPosition(unstretched);
+	result.translate(startPos.x, startPos.y);
+	return result;
+}
+
+Common::Point Channel::getPosition(bool unstretched) {
+	bool useOverride = isBboxDeterminedByChannel() && !unstretched;
+
+	return Common::Point(
 		useOverride ? _currentPoint.x : _sprite->_startPoint.x,
 		useOverride ? _currentPoint.y : _sprite->_startPoint.y
 	);
-	return result;
 }
 
 void Channel::setCast(CastMemberID memberID) {
diff --git a/engines/director/channel.h b/engines/director/channel.h
index 758ba5cf122..31937e35a54 100644
--- a/engines/director/channel.h
+++ b/engines/director/channel.h
@@ -45,6 +45,9 @@ public:
 
 	DirectorPlotData getPlotData();
 	const Graphics::Surface *getMask(bool forceMatte = false);
+
+	bool isBboxDeterminedByChannel();
+	Common::Point getPosition(bool unstretched = false);
 	// Return the area of screen to be used for drawing content.
 	Common::Rect getBbox(bool unstretched = false);
 
diff --git a/engines/director/graphics.cpp b/engines/director/graphics.cpp
index 55ce85c1a13..7f52cea2145 100644
--- a/engines/director/graphics.cpp
+++ b/engines/director/graphics.cpp
@@ -681,34 +681,22 @@ void DirectorPlotData::inkBlitSurface(Common::Rect &srcRect, const Graphics::Sur
 
 	srcPoint.y = abs(srcRect.top - destRect.top);
 	for (int i = 0; i < destRect.height(); i++, srcPoint.y++) {
-		if (d->_wm->_pixelformat.bytesPerPixel == 1) {
-			srcPoint.x = abs(srcRect.left - destRect.left);
-			const byte *msk = mask ? (const byte *)mask->getBasePtr(srcPoint.x, srcPoint.y) : nullptr;
-
-			for (int j = 0; j < destRect.width(); j++, srcPoint.x++) {
-				if (!srfClip.contains(srcPoint)) {
-					failedBoundsCheck = true;
-					continue;
-				}
+		srcPoint.x = abs(srcRect.left - destRect.left);
+		const byte *msk = mask ? (const byte *)mask->getBasePtr(srcPoint.x, srcPoint.y) : nullptr;
 
-				if (!mask || (msk && !(*msk++))) {
-					(d->getInkDrawPixel())(destRect.left + j, destRect.top + i,
-											preprocessColor(*((byte *)srf->getBasePtr(srcPoint.x, srcPoint.y))), this);
-				}
+		for (int j = 0; j < destRect.width(); j++, srcPoint.x++) {
+			if (!srfClip.contains(srcPoint)) {
+				failedBoundsCheck = true;
+				continue;
 			}
-		} else {
-			srcPoint.x = abs(srcRect.left - destRect.left);
-			const uint32 *msk = mask ? (const uint32 *)mask->getBasePtr(srcPoint.x, srcPoint.y) : nullptr;
 
-			for (int j = 0; j < destRect.width(); j++, srcPoint.x++) {
-				if (!srfClip.contains(srcPoint)) {
-					failedBoundsCheck = true;
-					continue;
-				}
-
-				if (!mask || (msk && !(*msk++))) {
+			if (!mask || (msk && (*msk++))) {
+				if (d->_wm->_pixelformat.bytesPerPixel == 1) {
+					(d->getInkDrawPixel())(destRect.left + j, destRect.top + i,
+										preprocessColor(*((byte *)srf->getBasePtr(srcPoint.x, srcPoint.y))), this);
+				} else {
 					(d->getInkDrawPixel())(destRect.left + j, destRect.top + i,
-											preprocessColor(*((uint32 *)srf->getBasePtr(srcPoint.x, srcPoint.y))), this);
+										preprocessColor(*((uint32 *)srf->getBasePtr(srcPoint.x, srcPoint.y))), this);
 				}
 			}
 		}
diff --git a/engines/director/window.cpp b/engines/director/window.cpp
index 3044ef6c561..96c2490043d 100644
--- a/engines/director/window.cpp
+++ b/engines/director/window.cpp
@@ -106,7 +106,7 @@ void Window::invertChannel(Channel *channel, const Common::Rect &destRect) {
 			const byte *msk = mask ? (const byte *)mask->getBasePtr(xoff, yoff + i) : nullptr;
 
 			for (int j = 0; j < srcRect.width(); j++, src++)
-				if (!mask || (msk && !(*msk++)))
+				if (!mask || (msk && (*msk++)))
 					*src = _wm->inverter(*src);
 		}
 	} else {
@@ -116,7 +116,7 @@ void Window::invertChannel(Channel *channel, const Common::Rect &destRect) {
 			const uint32 *msk = mask ? (const uint32 *)mask->getBasePtr(xoff, yoff + i) : nullptr;
 
 			for (int j = 0; j < srcRect.width(); j++, src++)
-				if (!mask || (msk && !(*msk++)))
+				if (!mask || (msk && (*msk++)))
 					*src = _wm->inverter(*src);
 		}
 	}


Commit: 61d06033048c537bfb755e2b31f858250a0e5c5f
    https://github.com/scummvm/scummvm/commit/61d06033048c537bfb755e2b31f858250a0e5c5f
Author: Scott Percival (code at moral.net.au)
Date: 2024-05-12T22:00:19+02:00

Commit Message:
DIRECTOR: Duplicate cast member fixes

Cast bounding boxes are now copied, and duplicates of shared cast
members are kept in the (cross-movie) shared cast.

Fixes displaying the inventory bar in Virtual Nightclub.

Changed paths:
    engines/director/cast.cpp
    engines/director/cast.h
    engines/director/castmember/bitmap.cpp
    engines/director/castmember/digitalvideo.cpp
    engines/director/castmember/filmloop.cpp
    engines/director/castmember/movie.cpp
    engines/director/castmember/shape.cpp
    engines/director/castmember/text.cpp
    engines/director/movie.cpp


diff --git a/engines/director/cast.cpp b/engines/director/cast.cpp
index 7a48f6e2fb6..ea9b89a5fef 100644
--- a/engines/director/cast.cpp
+++ b/engines/director/cast.cpp
@@ -234,7 +234,7 @@ CastMember *Cast::setCastMember(int castId, CastMember *cast) {
 	return cast;
 }
 
-bool Cast::duplicateCastMember(CastMember *source, int targetId) {
+bool Cast::duplicateCastMember(CastMember *source, CastMemberInfo *info, int targetId) {
 	if (_loadedCast->contains(targetId)) {
 		eraseCastMember(targetId);
 	}
@@ -277,6 +277,10 @@ bool Cast::duplicateCastMember(CastMember *source, int targetId) {
 		break;
 	}
 
+	if (info) {
+		CastMemberInfo *newInfo = new CastMemberInfo(*info);
+		_castsInfo[targetId] = newInfo;
+	}
 	setCastMember(targetId, target);
 	return true;
 }
@@ -286,9 +290,14 @@ bool Cast::eraseCastMember(int castId) {
 		CastMember *member = _loadedCast->getVal(castId);
 		delete member;
 		_loadedCast->erase(castId);
+
+		if (_castsInfo.contains(castId)) {
+			CastMemberInfo *info = _castsInfo.getVal(castId);
+			delete info;
+			_castsInfo.erase(castId);
+		}
 		return true;
 	}
-
 	return false;
 }
 
diff --git a/engines/director/cast.h b/engines/director/cast.h
index ac693e520c1..c760a1ec98b 100644
--- a/engines/director/cast.h
+++ b/engines/director/cast.h
@@ -103,7 +103,7 @@ public:
 	Common::Rect getCastMemberInitialRect(int castId);
 	void setCastMemberModified(int castId);
 	CastMember *setCastMember(int castId, CastMember *cast);
-	bool duplicateCastMember(CastMember *source, int targetId);
+	bool duplicateCastMember(CastMember *source, CastMemberInfo *info, int targetId);
 	bool eraseCastMember(int castId);
 	CastMember *getCastMember(int castId, bool load = true);
 	CastMember *getCastMemberByNameAndType(const Common::String &name, CastType type);
diff --git a/engines/director/castmember/bitmap.cpp b/engines/director/castmember/bitmap.cpp
index 1f11af9831b..52a9f082d5a 100644
--- a/engines/director/castmember/bitmap.cpp
+++ b/engines/director/castmember/bitmap.cpp
@@ -186,6 +186,10 @@ BitmapCastMember::BitmapCastMember(Cast *cast, uint16 castId, BitmapCastMember &
 	source.load();
 	_loaded = true;
 
+	_initialRect = source._initialRect;
+	_boundingRect = source._boundingRect;
+	_children = source._children;
+
 	_picture = source._picture ? new Picture(*source._picture) : nullptr;
 	_ditheredImg = nullptr;
 	_matte = nullptr;
diff --git a/engines/director/castmember/digitalvideo.cpp b/engines/director/castmember/digitalvideo.cpp
index 9087a17f067..5ac36d7bf2a 100644
--- a/engines/director/castmember/digitalvideo.cpp
+++ b/engines/director/castmember/digitalvideo.cpp
@@ -86,6 +86,10 @@ DigitalVideoCastMember::DigitalVideoCastMember(Cast *cast, uint16 castId, Digita
 	_type = kCastDigitalVideo;
 	_loaded = source._loaded;
 
+	_initialRect = source._initialRect;
+	_boundingRect = source._boundingRect;
+	_children = source._children;
+
 	_filename = source._filename;
 
 	_vflags = source._vflags;
diff --git a/engines/director/castmember/filmloop.cpp b/engines/director/castmember/filmloop.cpp
index f6ac16f0ed0..b35aacab033 100644
--- a/engines/director/castmember/filmloop.cpp
+++ b/engines/director/castmember/filmloop.cpp
@@ -53,6 +53,11 @@ FilmLoopCastMember::FilmLoopCastMember(Cast *cast, uint16 castId, FilmLoopCastMe
 	// force a load so we can copy the cast resource information
 	source.load();
 	_loaded = true;
+
+	_initialRect = source._initialRect;
+	_boundingRect = source._boundingRect;
+	_children = source._children;
+
 	_enableSound = source._enableSound;
 	_crop = source._crop;
 	_center = source._center;
diff --git a/engines/director/castmember/movie.cpp b/engines/director/castmember/movie.cpp
index 5e326188f50..65fa061536a 100644
--- a/engines/director/castmember/movie.cpp
+++ b/engines/director/castmember/movie.cpp
@@ -51,6 +51,10 @@ MovieCastMember::MovieCastMember(Cast *cast, uint16 castId, MovieCastMember &sou
 	_type = kCastMovie;
 	_loaded = source._loaded;
 
+	_initialRect = source._initialRect;
+	_boundingRect = source._boundingRect;
+	_children = source._children;
+
 	_flags = source._flags;
 	_looping = source._looping;
 	_enableScripts = source._enableScripts;
diff --git a/engines/director/castmember/shape.cpp b/engines/director/castmember/shape.cpp
index ab14751813f..7bcbb4030fa 100644
--- a/engines/director/castmember/shape.cpp
+++ b/engines/director/castmember/shape.cpp
@@ -85,6 +85,10 @@ ShapeCastMember::ShapeCastMember(Cast *cast, uint16 castId, ShapeCastMember &sou
 	_type = kCastShape;
 	_loaded = source._loaded;
 
+	_initialRect = source._initialRect;
+	_boundingRect = source._boundingRect;
+	_children = source._children;
+
 	_shapeType = source._shapeType;
 	_pattern = source._pattern;
 	_fillType = source._fillType;
diff --git a/engines/director/castmember/text.cpp b/engines/director/castmember/text.cpp
index 346ec0da44f..c76501f7ba5 100644
--- a/engines/director/castmember/text.cpp
+++ b/engines/director/castmember/text.cpp
@@ -189,6 +189,10 @@ TextCastMember::TextCastMember(Cast *cast, uint16 castId, TextCastMember &source
 	source.load();
 	_loaded = true;
 
+	_initialRect = source._initialRect;
+	_boundingRect = source._boundingRect;
+	_children = source._children;
+
 	_borderSize = source._borderSize;
 	_gutterSize = source._gutterSize;
 	_boxShadow = source._boxShadow;
diff --git a/engines/director/movie.cpp b/engines/director/movie.cpp
index 1d9640b15dc..f6a60a57f8f 100644
--- a/engines/director/movie.cpp
+++ b/engines/director/movie.cpp
@@ -452,19 +452,32 @@ bool Movie::eraseCastMember(CastMemberID memberID) {
 }
 
 bool Movie::duplicateCastMember(CastMemberID source, CastMemberID target) {
-	CastMember *sourceMember = getCastMember(source);
-	if (sourceMember) {
-		if (_casts.contains(target.castLib)) {
-			Cast *cast = _casts.getVal(target.castLib);
-			debugC(3, kDebugLoading, "Movie::DuplicateCastMember(): copying cast data from %s to %s (%s)", source.asString().c_str(), target.asString().c_str(), castType2str(sourceMember->_type));
-			return cast->duplicateCastMember(sourceMember, target.member);
-		} else {
-			warning("Movie::duplicateCastMember(): couldn't find destination castLib %d", target.castLib);
+	Cast *sourceCast = nullptr;
+	Cast *targetCast = nullptr;
+	if (_casts.contains(target.castLib)) {
+		if (_casts[target.castLib]->getCastMember(source.member)) {
+			sourceCast = _casts[target.castLib];
+		} else if (_sharedCast && _sharedCast->getCastMember(source.member)) {
+			sourceCast = _sharedCast;
 		}
-	} else {
+	}
+	// for shared + movie casts, duplications from the shared cast should be
+	// in the shared cast namespace
+	if (source.castLib == target.castLib) {
+		targetCast = sourceCast;
+	} else if (_casts.contains(target.castLib)) {
+		targetCast = _casts.getVal(target.castLib);
+	}
+	if (!sourceCast) {
 		warning("Movie::duplicateCastMember(): couldn't find source cast member %s", source.asString().c_str());
+	} else if (!targetCast) {
+		warning("Movie::duplicateCastMember(): couldn't find destination castLib %d", target.castLib);
+	} else {
+		CastMember *sourceMember = sourceCast->getCastMember(source.member);
+		CastMemberInfo *sourceInfo = sourceCast->getCastMemberInfo(source.member);
+		debugC(3, kDebugLoading, "Movie::DuplicateCastMember(): copying cast data from %s to %s (%s)", source.asString().c_str(), target.asString().c_str(), castType2str(sourceMember->_type));
+		return targetCast->duplicateCastMember(sourceMember, sourceInfo, target.member);
 	}
-
 	return false;
 }
 


Commit: e28f30d68019691c87aaea546b81ea8a3e806b15
    https://github.com/scummvm/scummvm/commit/e28f30d68019691c87aaea546b81ea8a3e806b15
Author: Scott Percival (code at moral.net.au)
Date: 2024-05-12T22:00:19+02:00

Commit Message:
DIRECTOR: XOBJ: Add readFile/writeFile stubs to MMovie

Changed paths:
    engines/director/lingo/lingo-builtins.cpp
    engines/director/lingo/xlibs/mmovie.cpp


diff --git a/engines/director/lingo/lingo-builtins.cpp b/engines/director/lingo/lingo-builtins.cpp
index 4b9f6713698..208b28d11d0 100644
--- a/engines/director/lingo/lingo-builtins.cpp
+++ b/engines/director/lingo/lingo-builtins.cpp
@@ -2049,7 +2049,9 @@ void LB::b_duplicate(int nargs) {
 	}
 
 	Score *score = movie->getScore();
+	// force redraw any sprites
 	score->refreshPointersForCastMemberID(*to.u.cast);
+	b_updateStage(0);
 	g_lingo->push(Datum(to.u.cast->member));
 }
 
diff --git a/engines/director/lingo/xlibs/mmovie.cpp b/engines/director/lingo/xlibs/mmovie.cpp
index 47bcd8dc990..a9efb835c96 100644
--- a/engines/director/lingo/xlibs/mmovie.cpp
+++ b/engines/director/lingo/xlibs/mmovie.cpp
@@ -20,7 +20,10 @@
  */
 
 #include "common/file.h"
+#include "common/savefile.h"
+#include "common/str.h"
 #include "common/system.h"
+#include "gui/filebrowser-dialog.h"
 #include "video/qt_decoder.h"
 
 #include "director/director.h"
@@ -501,7 +504,7 @@ void MMovieXObj::m_getMovieRate(int nargs) {
 void MMovieXObj::m_setMovieRate(int nargs) {
 	g_lingo->printSTUBWithArglist("MMovieXObj::m_setMovieRate", nargs);
 	if (nargs != 1) {
-		warning("MMovieXObj::m_setMovieRate: expecting 4 arguments!");
+		warning("MMovieXObj::m_setMovieRate: expecting 4 arguments");
 		g_lingo->dropStack(nargs);
 		g_lingo->push(Datum(0));
 		return;
@@ -513,8 +516,118 @@ void MMovieXObj::m_setMovieRate(int nargs) {
 
 XOBJSTUB(MMovieXObj::m_flushEvents, 0)
 XOBJSTUB(MMovieXObj::m_invalidateRect, 0)
-XOBJSTUB(MMovieXObj::m_readFile, "")
-XOBJSTUB(MMovieXObj::m_writeFile, "")
+
+void MMovieXObj::m_readFile(int nargs) {
+	g_lingo->printSTUBWithArglist("MMovieXObj::m_readFile", nargs);
+	if (nargs != 2) {
+		warning("MMovieXObj::m_readFile(): expecting 2 argument");
+	}
+	Common::SaveFileManager *saves = g_system->getSavefileManager();
+	bool scramble = (bool)g_lingo->pop().asInt();
+	Common::String origPath = g_lingo->pop().asString();
+	Common::String path = origPath;
+
+	Common::String prefix = g_director->getTargetName() + '-';
+	Common::String result;
+	if (origPath.empty()) {
+		Common::String mask = prefix + "*.txt";
+
+		GUI::FileBrowserDialog browser(nullptr, "txt", GUI::kFBModeLoad, mask.c_str());
+		if (browser.runModal() <= 0) {
+			debugC(5, kDebugXObj, "MMovieXObj::m_readFile(): read cancelled by modal");
+			g_lingo->push(result);
+		}
+		path = browser.getResult();
+
+	} else {
+		path = lastPathComponent(origPath, g_director->_dirSeparator);
+		if (path.hasSuffixIgnoreCase(".txt"))
+			path += ".txt";
+	}
+	if (!path.hasPrefixIgnoreCase(prefix)) {
+		path = prefix + path;
+	}
+
+	Common::SeekableReadStream *stream = saves->openForLoading(path);
+	if (stream) {
+		debugC(5, kDebugXObj, "MMovieXObj::m_readFile(): opening file %s as %s from the saves dir", origPath.c_str(), path.c_str());
+	} else {
+		Common::File *f = new Common::File;
+		Common::Path location = findPath(origPath);
+		if (!location.empty() && f->open(location)) {
+			debugC(5, kDebugXObj, "MMovieXObj::m_readFile(): opening file %s from the game dir", origPath.c_str());
+			stream = (Common::SeekableReadStream *)f;
+		} else {
+			delete f;
+		}
+	}
+
+	if (stream) {
+		while (!stream->eos() && !stream->err()) {
+			byte ch = stream->readByte();
+			if (scramble) // remove unbreakable encryption
+				ch ^= 0xa5;
+			result += ch;
+		}
+		delete stream;
+	} else {
+		warning("MMovieXObj::m_readFile(): file %s not found", origPath.c_str());
+	}
+
+	g_lingo->push(result);
+}
+
+void MMovieXObj::m_writeFile(int nargs) {
+	g_lingo->printSTUBWithArglist("MMovieXObj::m_writeFile", nargs);
+	if (nargs != 3) {
+		warning("MMovieXObj::m_writeFile(): expecting 3 arguments");
+	}
+	Common::SaveFileManager *saves = g_system->getSavefileManager();
+	bool scramble = (bool)g_lingo->pop().asInt();
+	Common::String data = g_lingo->pop().asString();
+	Common::String origPath = g_lingo->pop().asString();
+	Common::String path = origPath;
+	Common::String result;
+
+	Common::String prefix = g_director->getTargetName() + '-';
+	if (origPath.empty()) {
+		Common::String mask = prefix + "*.txt";
+
+		GUI::FileBrowserDialog browser(nullptr, "txt", GUI::kFBModeSave, mask.c_str());
+		if (browser.runModal() <= 0) {
+			debugC(5, kDebugXObj, "MMovieXObj::m_writeFile(): write cancelled by modal");
+			g_lingo->push(result);
+		}
+		path = browser.getResult();
+
+	} else {
+		path = lastPathComponent(origPath, g_director->_dirSeparator);
+		if (path.hasSuffixIgnoreCase(".txt"))
+			path += ".txt";
+	}
+	if (!path.hasPrefixIgnoreCase(prefix)) {
+		path = prefix + path;
+	}
+
+	Common::SeekableWriteStream *stream = saves->openForSaving(path);
+
+	if (stream) {
+		debugC(5, kDebugXObj, "MMovieXObj::m_writeFile(): opening file %s as %s from the saves dir", origPath.c_str(), path.c_str());
+		for (auto &it : data) {
+			byte ch = it;
+			if (scramble) // apply world's greatest encryption
+				ch ^= 0xa5;
+			stream->writeByte(ch);
+		}
+		stream->finalize();
+		delete stream;
+	} else {
+		warning("MMovieXObj::m_writeFile(): file %s not found", origPath.c_str());
+	}
+
+	g_lingo->push(result);
+}
+
 XOBJSTUB(MMovieXObj::m_copyFile, 0)
 XOBJSTUB(MMovieXObj::m_copyFileCont, 0)
 XOBJSTUB(MMovieXObj::m_freeSpace, 0)


Commit: e0242d8d6a1aabee535259c1a9df6927ea5966e5
    https://github.com/scummvm/scummvm/commit/e0242d8d6a1aabee535259c1a9df6927ea5966e5
Author: Scott Percival (code at moral.net.au)
Date: 2024-05-12T22:00:19+02:00

Commit Message:
DIRECTOR: Make b_random return 16-bit numbers

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


diff --git a/engines/director/lingo/lingo-builtins.cpp b/engines/director/lingo/lingo-builtins.cpp
index 208b28d11d0..0a1931fc495 100644
--- a/engines/director/lingo/lingo-builtins.cpp
+++ b/engines/director/lingo/lingo-builtins.cpp
@@ -445,8 +445,15 @@ void LB::b_power(int nargs) {
 }
 
 void LB::b_random(int nargs) {
-	Datum max = g_lingo->pop();
-	Datum res((int)(g_director->_rnd.getRandom(max.asInt()) + 1));
+	int max = g_lingo->pop().asInt();
+	Datum res;
+	// Output in D4/D5 seems to be bounded from 1-65535, regardless of input.
+	if (max <= 0) {
+		res = g_director->_rnd.getRandom(65535) + 1;
+	} else {
+		max = MIN(max, 65535);
+		res = g_director->_rnd.getRandom(max) + 1;
+	}
 	g_lingo->push(res);
 }
 


Commit: d7461b2f48c10d275439403477b271dc6811de91
    https://github.com/scummvm/scummvm/commit/d7461b2f48c10d275439403477b271dc6811de91
Author: Scott Percival (code at moral.net.au)
Date: 2024-05-12T22:00:19+02:00

Commit Message:
DIRECTOR: LINGO: Fix array eq/neq comparison bugs

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


diff --git a/engines/director/lingo/lingo-code.cpp b/engines/director/lingo/lingo-code.cpp
index 29238624b05..5dd3fbccb79 100644
--- a/engines/director/lingo/lingo-code.cpp
+++ b/engines/director/lingo/lingo-code.cpp
@@ -1307,13 +1307,23 @@ Datum LC::compareArrays(Datum (*compareFunc)(Datum, Datum), Datum d1, Datum d2,
 }
 
 Datum LC::eqData(Datum d1, Datum d2) {
-	// Lingo doesn't bother checking list equality if the left is longer
-	if (d1.isArray() && d2.isArray() &&
+	if ((d1.isArray() && d2.isArray()) || (d1.type == PARRAY && d2.type == PARRAY)) {
+		// D4 has a bug, and only checks the elements on the left array.
+		// Therefore if the left array is bigger, don't bother checking.
+		// LC::compareArrays will trim the inputs to the shortest length.
+		// (Mac 4.0.4 is fixed, Win 4.0.4 is not)
+		bool hasArrayBug = (g_director->getVersion() < 500 && g_director->getPlatform() == Common::kPlatformWindows) ||
+			(g_director->getVersion() < 404 && g_director->getPlatform() == Common::kPlatformMacintosh);
+		if (hasArrayBug &&
 			d1.u.farr->arr.size() > d2.u.farr->arr.size()) {
-		return Datum(0);
+			return Datum(0);
+		} else if (!hasArrayBug && d1.u.farr->arr.size() != d2.u.farr->arr.size()) {
+			// D5 and up is fixed; only check arrays if the sizes are the same.
+			return Datum(0);
+		}
 	}
 	if (d1.type == PARRAY && d2.type == PARRAY &&
-			d1.u.parr->arr.size() > d2.u.parr->arr.size()) {
+			d1.u.parr->arr.size() != d2.u.parr->arr.size()) {
 		return Datum(0);
 	}
 	if (d1.isArray() || d2.isArray() ||
@@ -1332,13 +1342,8 @@ void LC::c_eq() {
 }
 
 Datum LC::neqData(Datum d1, Datum d2) {
-	if (d1.isArray() || d2.isArray() ||
-			d1.type == PARRAY || d2.type == PARRAY) {
-		return LC::compareArrays(LC::neqData, d1, d2, false, true);
-	}
-	Datum check;
-	check = !d1.equalTo(d2, true);
-	return check;
+	// invert the output of eqData
+	return LC::eqData(d1, d2).asInt() ? 0 : 1;
 }
 
 void LC::c_neq() {
diff --git a/engines/director/lingo/tests/equality.lingo b/engines/director/lingo/tests/equality.lingo
index 17484be3cc9..ec9267ebcbf 100644
--- a/engines/director/lingo/tests/equality.lingo
+++ b/engines/director/lingo/tests/equality.lingo
@@ -58,6 +58,37 @@ scummvmAssert("a" <= "Bubba")
 scummvmAssert("z" > "Z")
 scummvmAssert("abba" > "Abba")
 
+-- Array comparison with coercion
+scummvmAssert([] = [])
+scummvmAssert(not([] <> []))
+scummvmAssert([1, 2] = [1, 2])
+scummvmAssert(not([1, 2] <> [1, 2]))
+scummvmAssert([1, 2] = [1, "2"])
+scummvmAssert(not([1, 2] <> [1, "2"]))
+scummvmAssert([1, 2, 3] = [1, "2", 3.0])
+scummvmAssert(not([1, 2, 3] <> [1, "2", 3.0]))
+
+-- D4 has a quirk where only the left side list elements are checked
+set the scummvmVersion to 400
+scummvmAssert([] = [1, "2", 4])
+scummvmAssert(not([] <> [1, "2", 4]))
+scummvmAssert([1, 2] = [1, "2", 4])
+scummvmAssert(not([1, 2] <> [1, "2", 4]))
+scummvmAssert([1, 2, 3] <> [1, "2", 4])
+scummvmAssert(not([1, 2, 3] = [1, "2", 4]))
+scummvmAssert([1, 2, 3] <> [1, "2"])
+scummvmAssert(not([1, 2, 3] = [1, "2"]))
+
+set the scummvmVersion to 500
+scummvmAssert([] <> [1, "2", 4])
+scummvmAssert(not([] = [1, "2", 4]))
+scummvmAssert([1, 2] <> [1, "2", 4])
+scummvmAssert(not([1, 2] = [1, "2", 4]))
+scummvmAssert([1, 2, 3] <> [1, "2", 4])
+scummvmAssert(not([1, 2, 3] = [1, "2", 4]))
+scummvmAssert([1, 2, 3] <> [1, "2"])
+scummvmAssert(not([1, 2, 3] = [1, "2"]))
+
 -- Void comparison
 set v1 = value("!")
 set v2 = value("!")


Commit: 5cacbd49e3d3d4ae5f9bf720a745c43a2689069c
    https://github.com/scummvm/scummvm/commit/5cacbd49e3d3d4ae5f9bf720a745c43a2689069c
Author: Scott Percival (code at moral.net.au)
Date: 2024-05-12T22:00:19+02:00

Commit Message:
DIRECTOR: Fix Frame::readSprite debug messages

Changed paths:
    engines/director/frame.cpp


diff --git a/engines/director/frame.cpp b/engines/director/frame.cpp
index a8ff9d56660..749f49010cb 100644
--- a/engines/director/frame.cpp
+++ b/engines/director/frame.cpp
@@ -267,13 +267,11 @@ void Frame::readSpriteD2(Common::MemoryReadStreamEndian &stream, uint16 offset,
 
 	uint16 fieldPosition = offset - spriteStart;
 
+	debugC(3, kDebugLoading, "Frame::readSpriteD2(): sprite: %d offset: %d size: %d, field: %d", spritePosition, offset, size, fieldPosition);
 	if (debugChannelSet(8, kDebugLoading)) {
-		debugC(8, kDebugLoading, "Frame::readSpriteD2(): channel %d, 16 bytes", spritePosition);
-		stream.hexdump(kSprChannelSizeD2);
+		stream.hexdump(size);
 	}
 
-	debugC(3, kDebugLoading, "Frame::readSpriteD2(): sprite: %d offset: %d size: %d, field: %d", spritePosition, offset, size, fieldPosition);
-
 	Sprite &sprite = *_sprites[spritePosition + 1];
 
 	uint32 initPos = stream.pos();
@@ -577,13 +575,11 @@ void Frame::readSpriteD4(Common::MemoryReadStreamEndian &stream, uint16 offset,
 
 	uint16 fieldPosition = offset - spriteStart;
 
+	debugC(3, kDebugLoading, "Frame::readSpriteD4(): sprite: %d offset: %d size: %d, field: %d", spritePosition, offset, size, fieldPosition);
 	if (debugChannelSet(8, kDebugLoading)) {
-		debugC(8, kDebugLoading, "Frame::readSpriteD4(): channel %d, 20 bytes", spritePosition);
-		stream.hexdump(kSprChannelSizeD4);
+		stream.hexdump(size);
 	}
 
-	debugC(3, kDebugLoading, "Frame::readSpriteD4(): sprite: %d offset: %d size: %d, field: %d", spritePosition, offset, size, fieldPosition);
-
 	Sprite &sprite = *_sprites[spritePosition + 1];
 
 	uint32 initPos = stream.pos();
@@ -881,13 +877,11 @@ void Frame::readSpriteD5(Common::MemoryReadStreamEndian &stream, uint16 offset,
 
 	uint16 fieldPosition = offset - spriteStart;
 
+	debugC(3, kDebugLoading, "Frame::readSpriteD5(): sprite: %d offset: %d size: %d, field: %d", spritePosition, offset, size, fieldPosition);
 	if (debugChannelSet(8, kDebugLoading)) {
-		debugC(8, kDebugLoading, "Frame::readSpriteD5(): channel %d, 20 bytes", spritePosition);
-		stream.hexdump(kSprChannelSizeD4);
+		stream.hexdump(size);
 	}
 
-	debugC(3, kDebugLoading, "Frame::readSpriteD5(): sprite: %d offset: %d size: %d, field: %d", spritePosition, offset, size, fieldPosition);
-
 	Sprite &sprite = *_sprites[spritePosition + 1];
 
 	uint32 initPos = stream.pos();
@@ -1081,13 +1075,11 @@ void Frame::readSpriteD6(Common::MemoryReadStreamEndian &stream, uint16 offset,
 
 	uint16 fieldPosition = offset - spriteStart;
 
+	debugC(3, kDebugLoading, "Frame::readSpriteD6(): sprite: %d offset: %d size: %d, field: %d", spritePosition, offset, size, fieldPosition);
 	if (debugChannelSet(8, kDebugLoading)) {
-		debugC(8, kDebugLoading, "Frame::readSpriteD6(): channel %d, 20 bytes", spritePosition);
-		stream.hexdump(kSprChannelSizeD6);
+		stream.hexdump(size);
 	}
 
-	debugC(3, kDebugLoading, "Frame::readSpriteD6(): sprite: %d offset: %d size: %d, field: %d", spritePosition, offset, size, fieldPosition);
-
 	Sprite &sprite = *_sprites[spritePosition + 1];
 
 	uint32 initPos = stream.pos();


Commit: d1a739fbce6299232f7d69a75dfa5a6544ec7868
    https://github.com/scummvm/scummvm/commit/d1a739fbce6299232f7d69a75dfa5a6544ec7868
Author: Scott Percival (code at moral.net.au)
Date: 2024-05-12T22:00:19+02:00

Commit Message:
DIRECTOR: Fix scaling of sprites in score frames

Sprites in a score frame have a width and a height; it appears these
values are only to be used if the "stretch" flag is set. Otherwise, the
cast dimensions are used.

Fixes the scaling when picking up the photo album from the filing
cabinet in P.A.W.S.

Changed paths:
    engines/director/channel.cpp
    engines/director/score.cpp
    engines/director/sprite.cpp
    engines/director/sprite.h


diff --git a/engines/director/channel.cpp b/engines/director/channel.cpp
index ad7ac0a4dd4..ccad39a2bb2 100644
--- a/engines/director/channel.cpp
+++ b/engines/director/channel.cpp
@@ -453,7 +453,7 @@ void Channel::setClean(Sprite *nextSprite, bool partial) {
 	// if cast are modified, then we need to replace it
 	// if cast size are changed, and we may need to replace it, because we may having the scaled bitmap castmember
 	// other situation, e.g. position changing, we will let channel to handle it. So we don't have to replace widget
-	bool dimsChanged = !isStretched() && !hasTextCastMember(_sprite) && (_sprite->_width != nextSprite->_width || _sprite->_height != nextSprite->_height);
+	bool dimsChanged = !hasTextCastMember(_sprite) && (_sprite->_width != nextSprite->_width || _sprite->_height != nextSprite->_height);
 
 	// if spriteType is changing, then we may need to re-create the widget since spriteType will guide when we creating widget
 	bool spriteTypeChanged = _sprite->_spriteType != nextSprite->_spriteType;
diff --git a/engines/director/score.cpp b/engines/director/score.cpp
index 384ede43976..38ba066f633 100644
--- a/engines/director/score.cpp
+++ b/engines/director/score.cpp
@@ -1706,7 +1706,7 @@ Frame *Score::getFrameData(int frameNum){
 void Score::setSpriteCasts() {
 	// Update sprite cache of cast pointers/info
 	for (uint16 j = 0; j < _currentFrame->_sprites.size(); j++) {
-		_currentFrame->_sprites[j]->setCast(_currentFrame->_sprites[j]->_castId);
+		_currentFrame->_sprites[j]->setCast(_currentFrame->_sprites[j]->_castId, !_currentFrame->_sprites[j]->_stretch);
 
 		debugC(8, kDebugLoading, "Score::setSpriteCasts(): Frame: 0 Channel: %d castId: %s type: %d (%s)",
 			 j, _currentFrame->_sprites[j]->_castId.asString().c_str(), _currentFrame->_sprites[j]->_spriteType,
diff --git a/engines/director/sprite.cpp b/engines/director/sprite.cpp
index 7645bf2fb9a..351169fae13 100644
--- a/engines/director/sprite.cpp
+++ b/engines/director/sprite.cpp
@@ -419,7 +419,7 @@ bool Sprite::checkSpriteType() {
 	return true;
 }
 
-void Sprite::setCast(CastMemberID memberID) {
+void Sprite::setCast(CastMemberID memberID, bool replaceDims) {
 	/**
 	 * There are two things we need to take into account here:
 	 *   1. The cast member's type
@@ -455,19 +455,21 @@ void Sprite::setCast(CastMemberID memberID) {
 			}
 		}
 
-		Common::Rect dims = _cast->getInitialRect();
-		switch (_cast->_type) {
-		case kCastBitmap:
-			_width = dims.width();
-			_height = dims.height();
-			break;
-		case kCastShape:
-		case kCastText: 	// fall-through
-			break;
-		default:
-			_width = dims.width();
-			_height = dims.height();
-			break;
+		if (replaceDims) {
+			Common::Rect dims = _cast->getInitialRect();
+			switch (_cast->_type) {
+			case kCastBitmap:
+				_width = dims.width();
+				_height = dims.height();
+				break;
+			case kCastShape:
+			case kCastText: 	// fall-through
+				break;
+			default:
+				_width = dims.width();
+				_height = dims.height();
+				break;
+			}
 		}
 
 	} else {
diff --git a/engines/director/sprite.h b/engines/director/sprite.h
index 576e5bd31a4..6a13a3fabf5 100644
--- a/engines/director/sprite.h
+++ b/engines/director/sprite.h
@@ -73,7 +73,7 @@ public:
 	uint16 getPattern();
 	void setPattern(uint16 pattern);
 
-	void setCast(CastMemberID memberID);
+	void setCast(CastMemberID memberID, bool replaceDims = true);
 	bool isQDShape();
 	Graphics::Surface *getQDMatte();
 	void createQDMatte();


Commit: 0bbed1793e0a917f324e40e54245aed7d66fb9e4
    https://github.com/scummvm/scummvm/commit/0bbed1793e0a917f324e40e54245aed7d66fb9e4
Author: Scott Percival (code at moral.net.au)
Date: 2024-05-12T22:00:19+02:00

Commit Message:
COMMON: Silence FSDirectory::createReadStreamForMemberAltStream warning

Components such as Common::MacResManager call this method to check if an
alternate stream exists; a null response is valid, so move this to the
debug log.

Changed paths:
    common/fs.cpp


diff --git a/common/fs.cpp b/common/fs.cpp
index 147d7ae64db..be0da790b71 100644
--- a/common/fs.cpp
+++ b/common/fs.cpp
@@ -402,7 +402,7 @@ SeekableReadStream *FSDirectory::createReadStreamForMemberAltStream(const Path &
 
 	SeekableReadStream *stream = node->createReadStreamForAltStream(altStreamType);
 	if (!stream)
-		warning("FSDirectory::createReadStreamForMemberAltStream: Can't create stream for file '%s' alt stream type %i", Common::toPrintable(path.toString(Common::Path::kNativeSeparator)).c_str(), static_cast<int>(altStreamType));
+		debug(5, "FSDirectory::createReadStreamForMemberAltStream: Can't create stream for file '%s' alt stream type %i", Common::toPrintable(path.toString(Common::Path::kNativeSeparator)).c_str(), static_cast<int>(altStreamType));
 
 	return stream;
 }


Commit: 51b7d37e707e1938dcac9e36a21eeb9b5e1bde6b
    https://github.com/scummvm/scummvm/commit/51b7d37e707e1938dcac9e36a21eeb9b5e1bde6b
Author: Scott Percival (code at moral.net.au)
Date: 2024-05-12T22:00:19+02:00

Commit Message:
DIRECTOR: Fix kTheDuration of DigitalVideoCastMember

Fixes the aerodog introductory cutscene in P.A.W.S.

Changed paths:
    engines/director/castmember/digitalvideo.cpp


diff --git a/engines/director/castmember/digitalvideo.cpp b/engines/director/castmember/digitalvideo.cpp
index 5ac36d7bf2a..c1f2cd95701 100644
--- a/engines/director/castmember/digitalvideo.cpp
+++ b/engines/director/castmember/digitalvideo.cpp
@@ -171,6 +171,8 @@ bool DigitalVideoCastMember::loadVideo(Common::String path) {
 		_video->setDitheringPalette(palette);
 	}
 
+	_duration = getMovieTotalTime();
+
 	return result;
 }
 
@@ -225,8 +227,6 @@ void DigitalVideoCastMember::startVideo() {
 
 	if (_channel && _channel->_stopTime == 0)
 		_channel->_stopTime = getMovieTotalTime();
-
-	_duration = getMovieTotalTime();
 }
 
 void DigitalVideoCastMember::stopVideo() {
@@ -311,7 +311,6 @@ Graphics::MacWidget *DigitalVideoCastMember::createWidget(Common::Rect &bbox, Ch
 uint DigitalVideoCastMember::getDuration() {
 	if (!_video || !_video->isVideoLoaded()) {
 		loadVideoFromCast();
-		_duration = getMovieTotalTime();
 	}
 	return _duration;
 }


Commit: ed4d4e355b298871503b110acebea60231aae0e0
    https://github.com/scummvm/scummvm/commit/ed4d4e355b298871503b110acebea60231aae0e0
Author: Scott Percival (code at moral.net.au)
Date: 2024-05-12T22:00:19+02:00

Commit Message:
DIRECTOR: Fix absolute paths in linked cast members

Fixes listening to the radio in the hotel in GADGET: Invention, Travel
and Adventure.

Changed paths:
    engines/director/cast.cpp
    engines/director/cast.h
    engines/director/castmember/bitmap.cpp
    engines/director/castmember/sound.cpp


diff --git a/engines/director/cast.cpp b/engines/director/cast.cpp
index ea9b89a5fef..4767d3f2228 100644
--- a/engines/director/cast.cpp
+++ b/engines/director/cast.cpp
@@ -726,6 +726,18 @@ void Cast::loadCast() {
 	}
 }
 
+Common::String Cast::getLinkedPath(int castId) {
+	if (!_castsInfo.contains(castId))
+		return Common::String();
+	Common::String filename = _castsInfo[castId]->fileName;
+	if (filename.empty())
+		return Common::String();
+	Common::String directory = _castsInfo[castId]->directory;
+	if (directory.lastChar() != g_director->_dirSeparator)
+		directory += g_director->_dirSeparator;
+	return directory + filename;
+}
+
 Common::String Cast::getVideoPath(int castId) {
 	Common::String res;
 	CastMember *cast = _loadedCast->getVal(castId);
@@ -760,12 +772,7 @@ Common::String Cast::getVideoPath(int castId) {
 
 	if (videoData == nullptr || videoData->size() == 0) {
 		// video file is linked, load from the filesystem
-
-		Common::String filename = _castsInfo[castId]->fileName;
-		Common::String directory = _castsInfo[castId]->directory;
-		if (directory.lastChar() != g_director->_dirSeparator)
-			directory += g_director->_dirSeparator;
-		res = directory + filename;
+		res = getLinkedPath(castId);
 	} else {
 		Video::QuickTimeDecoder qt;
 		qt.loadStream(videoData);
diff --git a/engines/director/cast.h b/engines/director/cast.h
index c760a1ec98b..aa3eedc6f7f 100644
--- a/engines/director/cast.h
+++ b/engines/director/cast.h
@@ -110,6 +110,7 @@ public:
 	CastMember *getCastMemberByScriptId(int scriptId);
 	CastMemberInfo *getCastMemberInfo(int castId);
 	const Stxt *getStxt(int castId);
+	Common::String getLinkedPath(int castId);
 	Common::String getVideoPath(int castId);
 	Common::SeekableReadStreamEndian *getResource(uint32 tag, uint16 id);
 	void rebuildCastNameCache();
diff --git a/engines/director/castmember/bitmap.cpp b/engines/director/castmember/bitmap.cpp
index 52a9f082d5a..879f5c4dc8e 100644
--- a/engines/director/castmember/bitmap.cpp
+++ b/engines/director/castmember/bitmap.cpp
@@ -554,12 +554,11 @@ void BitmapCastMember::load() {
 			}
 		}
 
-		CastMemberInfo *ci = _cast->getCastMemberInfo(_castId);
+		Common::String imageFilename = _cast->getLinkedPath(_castId);
 
 		if ((pic == nullptr || pic->size() == 0)
-				&& ci && !ci->fileName.empty()) {
+				&& !imageFilename.empty()) {
 			// image file is linked, load from the filesystem
-			Common::String imageFilename = ci->directory + g_director->_dirSeparator + ci->fileName;
 			Common::Path location = findPath(imageFilename);
 			Common::SeekableReadStream *file = Common::MacResManager::openFileOrDataFork(location);
 			if (file) {
diff --git a/engines/director/castmember/sound.cpp b/engines/director/castmember/sound.cpp
index 8eb3916f082..c87769f83f7 100644
--- a/engines/director/castmember/sound.cpp
+++ b/engines/director/castmember/sound.cpp
@@ -88,12 +88,11 @@ void SoundCastMember::load() {
 
 	if (sndData == nullptr || sndData->size() == 0) {
 		// audio file is linked, load from the filesystem
-		CastMemberInfo *ci = _cast->getCastMemberInfo(_castId);
-		if (ci) {
-			Common::String filename = ci->directory + g_director->_dirSeparator + ci->fileName;
+		Common::String res = _cast->getLinkedPath(_castId);
+		if (!res.empty()) {
 
-			debugC(2, kDebugLoading, "****** Loading file '%s', cast id: %d", filename.c_str(), sndId);
-			AudioFileDecoder *audio = new AudioFileDecoder(filename);
+			debugC(2, kDebugLoading, "****** Loading file '%s', cast id: %d", res.c_str(), sndId);
+			AudioFileDecoder *audio = new AudioFileDecoder(res);
 			_audio = audio;
 
 			// Linked sound files always have the loop flag disabled


Commit: a08934a95ea1a336429295a268a0f33665339619
    https://github.com/scummvm/scummvm/commit/a08934a95ea1a336429295a268a0f33665339619
Author: Scott Percival (code at moral.net.au)
Date: 2024-05-12T22:00:19+02:00

Commit Message:
DIRECTOR: Update current frame before running startMovie

The startMovie handler needs "the frame" to return the starting frame,
which isn't necessarily 1. Easiest way to fix this is to set up the
frame data in the Score::startPlay() handler.

Fixes the broken navigation in the Empire State Building in Hell Cab.

Changed paths:
    engines/director/score.cpp
    engines/director/score.h


diff --git a/engines/director/score.cpp b/engines/director/score.cpp
index 38ba066f633..3dbe4932b8a 100644
--- a/engines/director/score.cpp
+++ b/engines/director/score.cpp
@@ -85,7 +85,7 @@ Score::Score(Movie *movie) {
 	_numChannelsDisplayed = 0;
 	_skipTransition = false;
 
-	_curFrameNumber = 0;
+	_curFrameNumber = 1;
 	_framesStream = nullptr;
 	_currentFrame = nullptr;
 }
@@ -271,7 +271,6 @@ int Score::getPreviousLabelNumber(int referenceFrame) {
 }
 
 void Score::startPlay() {
-	_curFrameNumber = 1;
 	_playState = kPlayStarted;
 	_nextFrameTime = 0;
 	_nextFrameDelay = 0;
@@ -283,6 +282,9 @@ void Score::startPlay() {
 		return;
 	}
 
+	// load first frame (either 1 or _nextFrame)
+	updateCurrentFrame();
+
 	// All frames in the same movie have the same number of channels
 	if (_playState != kPlayStopped)
 		for (uint i = 0; i < _currentFrame->_sprites.size(); i++)
@@ -338,6 +340,10 @@ void Score::setDelay(uint32 ticks) {
 	}
 }
 
+void Score::setCurrentFrame(uint16 frameId) {
+	_nextFrame = frameId;
+}
+
 bool Score::isWaitingForNextFrame() {
 	bool keepWaiting = false;
 	debugC(8, kDebugLoading, "Score::isWaitingForNextFrame(): nextFrameTime: %d, time: %d, sound: %d, click: %d, video: %d", _nextFrameTime, g_system->getMillis(false), _waitForChannel, _waitForClick, _waitForVideoChannel);
diff --git a/engines/director/score.h b/engines/director/score.h
index 4f92c5ed2bc..0e4191fbe71 100644
--- a/engines/director/score.h
+++ b/engines/director/score.h
@@ -90,7 +90,7 @@ public:
 	void stopPlay();
 	void setDelay(uint32 ticks);
 
-	void setCurrentFrame(uint16 frameId) { _nextFrame = frameId; }
+	void setCurrentFrame(uint16 frameId);
 	uint16 getCurrentFrameNum() { return _curFrameNumber; }
 	int getNextFrame() { return _nextFrame; }
 	uint16 getFramesNum() { return _numFrames; }


Commit: 32f3cd0633a9001075189d51fe5e1ef03af0ec47
    https://github.com/scummvm/scummvm/commit/32f3cd0633a9001075189d51fe5e1ef03af0ec47
Author: Scott Percival (code at moral.net.au)
Date: 2024-05-12T22:00:19+02:00

Commit Message:
DIRECTOR: Add more debugging to FilmLoopCastMember

Changed paths:
    engines/director/castmember/filmloop.cpp


diff --git a/engines/director/castmember/filmloop.cpp b/engines/director/castmember/filmloop.cpp
index b35aacab033..251d3f51d1f 100644
--- a/engines/director/castmember/filmloop.cpp
+++ b/engines/director/castmember/filmloop.cpp
@@ -85,7 +85,7 @@ Common::Array<Channel> *FilmLoopCastMember::getSubChannels(Common::Rect &bbox, C
 	_subchannels.clear();
 
 	if (channel->_filmLoopFrame >= _frames.size()) {
-		warning("Film loop frame %d requested, only %d available", channel->_filmLoopFrame, _frames.size());
+		warning("FilmLoopCastMember::getSubChannels(): Film loop frame %d requested, only %d available", channel->_filmLoopFrame, _frames.size());
 		return &_subchannels;
 	}
 
@@ -96,6 +96,15 @@ Common::Array<Channel> *FilmLoopCastMember::getSubChannels(Common::Rect &bbox, C
 	}
 	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(),
+			_initialRect.left + _initialRect.width()/2,
+			_initialRect.top + _initialRect.height()/2,
+			_initialRect.width(), _initialRect.height(),
+			bbox.left + bbox.width()/2,
+			bbox.top + bbox.height()/2,
+			bbox.width(), bbox.height());
+
 	// copy the sprites in order to the list
 	for (auto &iter : spriteIds) {
 		Sprite src = _frames[channel->_filmLoopFrame].sprites[iter];
@@ -109,6 +118,11 @@ Common::Array<Channel> *FilmLoopCastMember::getSubChannels(Common::Rect &bbox, C
 		int16 width = src._width * widgetRect.width() / _initialRect.width();
 		int16 height = src._height * widgetRect.height() / _initialRect.height();
 
+		debugC(5, kDebugImages, "FilmLoopCastMember::getSubChannels(): sprite: %d - cast: %s, orig: %d,%d %dx%d, trans: %d,%d %dx%d",
+				iter, src._castId.asString().c_str(),
+				src._startPoint.x, src._startPoint.y, src._width, src._height,
+				absX, absY, width, height);
+
 		// Re-inject the translated position into the Sprite.
 		// This saves the hassle of having to force the Channel to be in puppet mode.
 		src._width = width;
@@ -121,7 +135,6 @@ Common::Array<Channel> *FilmLoopCastMember::getSubChannels(Common::Rect &bbox, C
 		// that's only for querying the constraint channel which is not used.
 		Channel chan(nullptr, &src);
 		_subchannels.push_back(chan);
-
 	}
 	// Initialise the widgets on all of the subchannels.
 	// This has to be done once the list has been constructed, otherwise


Commit: 1bb44c5241859cf987c4cd59d92a70a3ce2d8c1b
    https://github.com/scummvm/scummvm/commit/1bb44c5241859cf987c4cd59d92a70a3ce2d8c1b
Author: Scott Percival (code at moral.net.au)
Date: 2024-05-12T22:00:19+02:00

Commit Message:
DIRECTOR: Add locH and locV property support for POINT

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


diff --git a/engines/director/lingo/lingo-the.cpp b/engines/director/lingo/lingo-the.cpp
index b9e5a58e65b..5f49ba15ef6 100644
--- a/engines/director/lingo/lingo-the.cpp
+++ b/engines/director/lingo/lingo-the.cpp
@@ -1963,6 +1963,18 @@ void Lingo::getObjectProp(Datum &obj, Common::String &propName) {
 		g_debugger->propReadHook(propName);
 		return;
 	}
+	if (obj.type == POINT) {
+		if (propName.equalsIgnoreCase("locH")) {
+			d = obj.u.farr->arr[0];
+		} else if (propName.equalsIgnoreCase("locV")) {
+			d = obj.u.farr->arr[1];
+		} else {
+			g_lingo->lingoError("Lingo::getObjectProp: Point <%s> has no property '%s'", obj.asString(true).c_str(), propName.c_str());
+		}
+		g_lingo->push(d);
+		g_debugger->propReadHook(propName);
+		return;
+	}
 	if (obj.type == RECT) {
 		if (propName.equalsIgnoreCase("left")) {
 			d = obj.u.farr->arr[0];


Commit: 64cc8adb3cf71dd6e9a64af62533a91ff1a099b3
    https://github.com/scummvm/scummvm/commit/64cc8adb3cf71dd6e9a64af62533a91ff1a099b3
Author: Scott Percival (code at moral.net.au)
Date: 2024-05-12T22:00:19+02:00

Commit Message:
DIRECTOR: LINGO: Allow b_length(VOID)

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


diff --git a/engines/director/lingo/lingo-builtins.cpp b/engines/director/lingo/lingo-builtins.cpp
index 0a1931fc495..00674e6117d 100644
--- a/engines/director/lingo/lingo-builtins.cpp
+++ b/engines/director/lingo/lingo-builtins.cpp
@@ -524,7 +524,7 @@ void LB::b_charToNum(int nargs) {
 
 void LB::b_length(int nargs) {
 	Datum d = g_lingo->pop();
-	if (d.type == INT || d.type == FLOAT) {
+	if (d.type == INT || d.type == FLOAT || d.type == VOID) {
 		g_lingo->push(0);
 		return;
 	}


Commit: 10ac6b61d8d4dd1a0616a1989cbd7838b3097db5
    https://github.com/scummvm/scummvm/commit/10ac6b61d8d4dd1a0616a1989cbd7838b3097db5
Author: Scott Percival (code at moral.net.au)
Date: 2024-05-12T22:00:19+02:00

Commit Message:
DIRECTOR: LINGO: Add guardrail for implicit factory calls

It is possible to have a script which looks like a factory,
but has normal functions in it also. Calling those normal functions
should not apply the bodge which injects the me object as argument 1.

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


diff --git a/engines/director/lingo/lingo-code.cpp b/engines/director/lingo/lingo-code.cpp
index 5dd3fbccb79..8e804816528 100644
--- a/engines/director/lingo/lingo-code.cpp
+++ b/engines/director/lingo/lingo-code.cpp
@@ -1554,10 +1554,13 @@ void LC::call(const Common::String &name, int nargs, bool allowRetVal) {
 	if (g_lingo->_state->me.type == OBJECT) {
 		AbstractObject *target = g_lingo->_state->me.u.obj;
 		funcSym = target->getMethod(name);
-		if (funcSym.type != VOIDSYM) {
+		if (name.hasPrefixIgnoreCase("m") && funcSym.type != VOIDSYM) {
 			if (nargs == 0) {
+				debugC(3, kDebugLingoExec, "Factory method call detected with missing first arg");
 				g_lingo->_stack.push_back(Datum());
 				nargs = 1;
+			} else {
+				debugC(3, kDebugLingoExec, "Factory method call detected with invalid first arg: <%s>", g_lingo->_stack[g_lingo->_stack.size() - nargs].asString(true).c_str());
 			}
 			g_lingo->_stack[g_lingo->_stack.size() - nargs] = funcSym.target; // Set first arg to target
 			call(funcSym, nargs, allowRetVal);


Commit: 785fb478b62d081d86e27309a59fb7df5836f094
    https://github.com/scummvm/scummvm/commit/785fb478b62d081d86e27309a59fb7df5836f094
Author: Scott Percival (code at moral.net.au)
Date: 2024-05-12T22:00:19+02:00

Commit Message:
DIRECTOR: Fix null pointer in BitmapCastMember

Changed paths:
    engines/director/castmember/bitmap.cpp
    engines/director/picture.h


diff --git a/engines/director/castmember/bitmap.cpp b/engines/director/castmember/bitmap.cpp
index 879f5c4dc8e..8b4b1894272 100644
--- a/engines/director/castmember/bitmap.cpp
+++ b/engines/director/castmember/bitmap.cpp
@@ -43,7 +43,7 @@ namespace Director {
 BitmapCastMember::BitmapCastMember(Cast *cast, uint16 castId, Common::SeekableReadStreamEndian &stream, uint32 castTag, uint16 version, uint8 flags1)
 		: CastMember(cast, castId, stream) {
 	_type = kCastBitmap;
-	_picture = nullptr;
+	_picture = new Picture();
 	_ditheredImg = nullptr;
 	_matte = nullptr;
 	_noMatte = false;
@@ -692,7 +692,7 @@ void BitmapCastMember::unload() {
 		return;
 
 	delete _picture;
-	_picture = nullptr;
+	_picture = new Picture();
 
 	delete _ditheredImg;
 	_ditheredImg = nullptr;
diff --git a/engines/director/picture.h b/engines/director/picture.h
index cfaee61546c..c88ae4aa1d3 100644
--- a/engines/director/picture.h
+++ b/engines/director/picture.h
@@ -39,6 +39,7 @@ struct Picture {
 		return _paletteColors * 3;
 	}
 
+	Picture() {}
 	Picture(Image::ImageDecoder &img);
 	Picture(Picture &picture);
 	~Picture();


Commit: f2a23a5e391f85998e020ceff0158f8b919e7284
    https://github.com/scummvm/scummvm/commit/f2a23a5e391f85998e020ceff0158f8b919e7284
Author: Scott Percival (code at moral.net.au)
Date: 2024-05-12T22:00:19+02:00

Commit Message:
DIRECTOR: Add cheat-enabling patch for Virtual Nightclub

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 baa90b8ce29..c2b91aacb08 100644
--- a/engines/director/lingo/lingo-patcher.cpp
+++ b/engines/director/lingo/lingo-patcher.cpp
@@ -320,6 +320,30 @@ on checkkaiwa kaiwatrue, kaiwafalse \r\
 end \r\
 ";
 
+/*
+ * Virtual Nightclub has a number of cheat codes for debugging.
+ * These are normally enabled by pressing Option + 0, however the
+ * released game has this code stubbed out with a return.
+ */
+
+const char *vncEnableCheats = " \
+on togCh\r\
+  if getFlag(#cheats) then\r\
+    setFlag(#cheats, 0)\r\
+    set the foreColor of field \"viewName_cast\" to 255\r\
+    alert(\"VNC Cheats off\")\r\
+  else\r\
+    if platform() < 256 then\r\
+      set the textFont of field \"viewName_cast\" to \"Monaco\"\r\
+    end if\r\
+    set the foreColor of field \"viewName_cast\" to 172\r\
+    set the textSize of field \"viewName_cast\" to 9\r\
+    setFlag(#cheats)\r\
+    alert(\"VNC Cheats on\")\r\
+  end if\r\
+end\r\
+";
+
 /* AMBER: Journeys Beyond has a check to ensure that the CD and hard disk data are on
  * different drive letters. ScummVM will pretend that every drive letter contains the
  * game contents, so we need to hotpatch the CD detection routine to return D:.
@@ -352,6 +376,7 @@ struct ScriptHandlerPatch {
 	{"kyoto", nullptr, kPlatformWindows, "ck_data\\opening\\shared.dxr", kMovieScript, 802, DEFAULT_CAST_LIB, &kyotoTextEntryFix},
 	{"kyoto", nullptr, kPlatformWindows, "ck_data\\rajoumon\\shared.dxr", kMovieScript, 840, DEFAULT_CAST_LIB, &kyotoTextEntryFix},
 	{"kyoto", nullptr, kPlatformWindows, "ck_data\\rokudou\\shared.dxr", kMovieScript, 846, DEFAULT_CAST_LIB, &kyotoTextEntryFix},
+	{"vnc", nullptr, kPlatformWindows, "VNC2\\SHARED.DXR", kMovieScript, 1248, DEFAULT_CAST_LIB, &vncEnableCheats},
 	{"amber", nullptr, kPlatformWindows, "AMBER_F\\AMBER_JB.EXE", kMovieScript, 7, DEFAULT_CAST_LIB, &amberDriveDetectionFix},
 	{nullptr, nullptr, kPlatformUnknown, nullptr, kNoneScript, 0, 0, nullptr},
 


Commit: ce447005da339e1835128151d1e5842ad5ce4ae4
    https://github.com/scummvm/scummvm/commit/ce447005da339e1835128151d1e5842ad5ce4ae4
Author: Scott Percival (code at moral.net.au)
Date: 2024-05-12T22:00:19+02:00

Commit Message:
DIRECTOR: XOBJ: MMovie fixes

Changed paths:
    engines/director/lingo/xlibs/mmovie.cpp
    engines/director/lingo/xlibs/mmovie.h


diff --git a/engines/director/lingo/xlibs/mmovie.cpp b/engines/director/lingo/xlibs/mmovie.cpp
index a9efb835c96..74dae9c01da 100644
--- a/engines/director/lingo/xlibs/mmovie.cpp
+++ b/engines/director/lingo/xlibs/mmovie.cpp
@@ -165,18 +165,21 @@ void MMovieXObject::updateScreenBlocking() {
 		bool keepPlaying = true;
 		if (g_system->getEventManager()->pollEvent(event)) {
 			switch (event.type) {
-				case Common::EVENT_QUIT:
-					g_director->processEventQUIT();
-					// fallthrough
-				case Common::EVENT_KEYDOWN:
-				case Common::EVENT_RBUTTONDOWN:
-				case Common::EVENT_LBUTTONDOWN:
-					if (_abortOnClick)
-						keepPlaying = false;
-					break;
-				default:
-					break;
+			case Common::EVENT_QUIT:
+				g_director->processEventQUIT();
+				// fallthrough
+			case Common::EVENT_KEYDOWN:
+			case Common::EVENT_RBUTTONDOWN:
+			case Common::EVENT_LBUTTONDOWN:
+				if (_abortOnClick)
+					keepPlaying = false;
+				break;
+			default:
+				break;
 			}
+			// pass event through to window manager.
+			// this is required so that e.g. the stillDown is kept up to date
+			g_director->_wm->processEvent(event);
 		}
 		if (!keepPlaying)
 			break;
@@ -192,7 +195,7 @@ void MMovieXObject::updateScreen() {
 			if (movie._video && movie._video->isPlaying() && movie._video->needsUpdate()) {
 				const Graphics::Surface *frame = movie._video->decodeNextFrame();
 				if (frame) {
-					debugC(5, kDebugXObj, "MMovieXObject: rendering movie %s (%d), time %d", movie._path.toString().c_str(), _currentMovieIndex, movie._video->getTime());
+					debugC(8, kDebugXObj, "MMovieXObject: rendering movie %s (%d), time %d", movie._path.toString().c_str(), _currentMovieIndex, movie._video->getTime());
 					Graphics::Surface *temp1 = frame->scale(_bounds.width(), _bounds.height(), false);
 					Graphics::Surface *temp2 = temp1->convertTo(g_director->_pixelformat, movie._video->getPalette());
 					g_system->copyRectToScreen(temp2->getPixels(), temp2->pitch, _bounds.left, _bounds.top, _bounds.width(), _bounds.height());
@@ -202,7 +205,7 @@ void MMovieXObject::updateScreen() {
 			}
 			// do a time check
 			uint32 endTime = Audio::Timestamp(0, seg._length + seg._start, movie._video->getTimeScale()).msecs();
-			debugC(5, kDebugXObj, "MMovieXObject::updateScreen(): time: %d, endTime: %d", movie._video->getTime(), endTime);
+			debugC(8, kDebugXObj, "MMovieXObject::updateScreen(): time: %d, endTime: %d", movie._video->getTime(), endTime);
 			if (movie._video->getTime() >= endTime) {
 				if (_looping) {
 					debugC(5, kDebugXObj, "MMovieXObject::updateScreen(): rewinding loop on %s (%d), time %d", movie._path.toString().c_str(), _currentMovieIndex, movie._video->getTime());
@@ -218,6 +221,16 @@ void MMovieXObject::updateScreen() {
 	g_director->delayMillis(10);
 }
 
+int MMovieXObject::getTicks() {
+	if (_currentMovieIndex) {
+		MMovieFile &movie = _movies.getVal(_currentMovieIndex);
+		if (movie._video) {
+			return movie._video->getTime() * 60 / 1000;
+		}
+	}
+	return -1;
+}
+
 void MMovieXObj::open(ObjectType type, const Common::Path &path) {
     MMovieXObject::initMethods(xlibMethods);
     MMovieXObject *xobj = new MMovieXObject(type);
@@ -277,10 +290,10 @@ void MMovieXObj::m_openMMovie(int nargs) {
 		movie._video = nullptr;
 	}
 	uint32 offsetCount = offsetsFile.readUint32BE();
-	offsetsFile.skip(0x3c);
+	offsetsFile.skip(0x3c); // rest of header should be blank
 	debugC(5, kDebugXObj, "MMovieXObj:m_openMMovie(): opening movie %s (index %d)", path.toString().c_str(), me->_lastIndex);
 	for (uint32 i = 0; i < offsetCount; i++) {
-		Common::String name = offsetsFile.readString(0x20, 0x10);
+		Common::String name = offsetsFile.readString(' ', 0x10);
 		uint32 start = offsetsFile.readUint32BE();
 		uint32 length = offsetsFile.readUint32BE();
 		debugC(5, kDebugXObj, "MMovieXObj:m_openMMovie(): adding segment %s (index %d): start %d (%dms) length %d (%dms)", name.c_str(), movie.segments.size(), start, Audio::Timestamp(0, start, movie._video->getTimeScale()).msecs(), length, Audio::Timestamp(0, length, movie._video->getTimeScale()).msecs());
@@ -351,7 +364,9 @@ void MMovieXObj::m_playSegment(int nargs) {
 				g_lingo->push(MMovieError::MMOVIE_INDEX_OUT_OF_RANGE);
 				return;
 			}
-			g_lingo->push(0);
+			int result = me->getTicks();
+			debugC(5, kDebugXObj, "MMovieXObj::m_playSegment: ticks: %d", result);
+			g_lingo->push(result);
 			return;
 		}
 	}
@@ -384,7 +399,9 @@ void MMovieXObj::m_playSegLoop(int nargs) {
 		if (it._value.segLookup.contains(segmentName)) {
 			int segIndex = it._value.segLookup.getVal(segmentName);
 			me->playSegment(it._key, segIndex, true, restore, shiftAbort, abortOnClick, purge, async);
-			g_lingo->push(0);
+			int result = me->getTicks();
+			debugC(5, kDebugXObj, "MMovieXObj::m_playSegLoop: ticks: %d", result);
+			g_lingo->push(result);
 			return;
 		}
 	}
@@ -393,13 +410,14 @@ void MMovieXObj::m_playSegLoop(int nargs) {
 }
 
 void MMovieXObj::m_idleSegment(int nargs) {
-	debugC(5, kDebugXObj, "MMovieXObj::m_idleSegment()");
 	if (nargs != 0) {
 		g_lingo->dropStack(nargs);
 	}
 	MMovieXObject *me = static_cast<MMovieXObject *>(g_lingo->_state->me.u.obj);
 	me->updateScreen();
-	g_lingo->push(0);
+	int result = me->getTicks();
+	debugC(5, kDebugXObj, "MMovieXObj::m_idleSegment(): ticks: %d", result);
+	g_lingo->push(result);
 }
 
 void MMovieXObj::m_stopSegment(int nargs) {
@@ -523,7 +541,7 @@ void MMovieXObj::m_readFile(int nargs) {
 		warning("MMovieXObj::m_readFile(): expecting 2 argument");
 	}
 	Common::SaveFileManager *saves = g_system->getSavefileManager();
-	bool scramble = (bool)g_lingo->pop().asInt();
+	bool scramble = g_lingo->pop().asInt() != 0;
 	Common::String origPath = g_lingo->pop().asString();
 	Common::String path = origPath;
 
@@ -536,6 +554,7 @@ void MMovieXObj::m_readFile(int nargs) {
 		if (browser.runModal() <= 0) {
 			debugC(5, kDebugXObj, "MMovieXObj::m_readFile(): read cancelled by modal");
 			g_lingo->push(result);
+			return;
 		}
 		path = browser.getResult();
 
@@ -583,7 +602,7 @@ void MMovieXObj::m_writeFile(int nargs) {
 		warning("MMovieXObj::m_writeFile(): expecting 3 arguments");
 	}
 	Common::SaveFileManager *saves = g_system->getSavefileManager();
-	bool scramble = (bool)g_lingo->pop().asInt();
+	bool scramble = g_lingo->pop().asInt() != 0;
 	Common::String data = g_lingo->pop().asString();
 	Common::String origPath = g_lingo->pop().asString();
 	Common::String path = origPath;
@@ -597,6 +616,7 @@ void MMovieXObj::m_writeFile(int nargs) {
 		if (browser.runModal() <= 0) {
 			debugC(5, kDebugXObj, "MMovieXObj::m_writeFile(): write cancelled by modal");
 			g_lingo->push(result);
+			return;
 		}
 		path = browser.getResult();
 
diff --git a/engines/director/lingo/xlibs/mmovie.h b/engines/director/lingo/xlibs/mmovie.h
index ac5a8d9fed5..ae97dc081ee 100644
--- a/engines/director/lingo/xlibs/mmovie.h
+++ b/engines/director/lingo/xlibs/mmovie.h
@@ -83,6 +83,7 @@ public:
 	bool stopSegment();
 	void updateScreenBlocking();
 	void updateScreen();
+	int getTicks();
 };
 
 namespace MMovieXObj {


Commit: 8b2a30dd65d91567dbcb723d1a68f98370cde684
    https://github.com/scummvm/scummvm/commit/8b2a30dd65d91567dbcb723d1a68f98370cde684
Author: Scott Percival (code at moral.net.au)
Date: 2024-05-12T22:00:19+02:00

Commit Message:
DIRECTOR: Rename Lingo::printSTUBWithArglist

Changed paths:
    engines/director/lingo/lingo-builtins.cpp
    engines/director/lingo/lingo-bytecode.cpp
    engines/director/lingo/lingo-code.cpp
    engines/director/lingo/lingo.h
    engines/director/lingo/xlibs/mmovie.cpp
    engines/director/lingo/xlibs/remixxcmd.cpp


diff --git a/engines/director/lingo/lingo-builtins.cpp b/engines/director/lingo/lingo-builtins.cpp
index 00674e6117d..812bfb30df9 100644
--- a/engines/director/lingo/lingo-builtins.cpp
+++ b/engines/director/lingo/lingo-builtins.cpp
@@ -306,9 +306,12 @@ void Lingo::cleanupBuiltIns(BuiltinProto protos[]) {
 	}
 }
 
-void Lingo::printSTUBWithArglist(const char *funcname, int nargs, const char *prefix) {
-	Common::String s(funcname);
+void Lingo::printArgs(const char *funcname, int nargs, const char *prefix) {
+	Common::String s;
+	if (prefix)
+		s += Common::String(prefix);
 
+	s += Common::String(funcname);
 	s += '(';
 
 	for (int i = 0; i < nargs; i++) {
@@ -322,7 +325,7 @@ void Lingo::printSTUBWithArglist(const char *funcname, int nargs, const char *pr
 
 	s += ")";
 
-	debug(3, "%s %s", prefix, s.c_str());
+	debug(3, "%s", s.c_str());
 }
 
 void Lingo::convertVOIDtoString(int arg, int nargs) {
diff --git a/engines/director/lingo/lingo-bytecode.cpp b/engines/director/lingo/lingo-bytecode.cpp
index e6a1d8d9d43..00da9e92575 100644
--- a/engines/director/lingo/lingo-bytecode.cpp
+++ b/engines/director/lingo/lingo-bytecode.cpp
@@ -428,7 +428,7 @@ void LC::cb_localcall() {
 	if ((nargs.type == ARGC) || (nargs.type == ARGCNORET)) {
 		Common::String name = g_lingo->_state->context->_functionNames[functionId];
 		if (debugChannelSet(3, kDebugLingoExec))
-			printWithArgList(name.c_str(), nargs.u.i, "localcall:");
+			g_lingo->printArgs(name.c_str(), nargs.u.i, "localcall:");
 
 		LC::call(name, nargs.u.i, nargs.type == ARGC);
 
diff --git a/engines/director/lingo/lingo-code.cpp b/engines/director/lingo/lingo-code.cpp
index 8e804816528..695636d2226 100644
--- a/engines/director/lingo/lingo-code.cpp
+++ b/engines/director/lingo/lingo-code.cpp
@@ -1500,7 +1500,7 @@ void LC::c_callfunc() {
 
 void LC::call(const Common::String &name, int nargs, bool allowRetVal) {
 	if (debugChannelSet(3, kDebugLingoExec))
-		printWithArgList(name.c_str(), nargs, "call:");
+		g_lingo->printArgs(name.c_str(), nargs, "call:");
 
 	Symbol funcSym;
 
diff --git a/engines/director/lingo/lingo.h b/engines/director/lingo/lingo.h
index 64c0700369a..7f3f8449fc2 100644
--- a/engines/director/lingo/lingo.h
+++ b/engines/director/lingo/lingo.h
@@ -51,7 +51,6 @@ class LingoCompiler;
 typedef void (*inst)(void);
 #define	STOP (inst)0
 #define ENTITY_INDEX(t,id) ((t) * 100000 + (id))
-#define printWithArgList g_lingo->printSTUBWithArglist
 
 int calcStringAlignment(const char *s);
 int calcCodeAlignment(int l);
@@ -413,7 +412,8 @@ public:
 	Datum getVoid();
 	void pushVoid();
 
-	void printSTUBWithArglist(const char *funcname, int nargs, const char *prefix = "STUB:");
+	void printArgs(const char *funcname, int nargs, const char *prefix = nullptr);
+	inline void printSTUBWithArglist(const char *funcname, int nargs) { printArgs(funcname, nargs, "STUB: "); }
 	void convertVOIDtoString(int arg, int nargs);
 	void dropStack(int nargs);
 	void drop(uint num);
diff --git a/engines/director/lingo/xlibs/mmovie.cpp b/engines/director/lingo/xlibs/mmovie.cpp
index 74dae9c01da..9030f13a0cc 100644
--- a/engines/director/lingo/xlibs/mmovie.cpp
+++ b/engines/director/lingo/xlibs/mmovie.cpp
@@ -245,7 +245,7 @@ void MMovieXObj::close(ObjectType type) {
 }
 
 void MMovieXObj::m_new(int nargs) {
-	g_lingo->printSTUBWithArglist("MMovieXObj::m_new", nargs);
+	g_lingo->printArgs("MMovieXObj::m_new", nargs);
 	g_lingo->dropStack(nargs);
 	g_lingo->push(g_lingo->_state->me);
 }
@@ -254,7 +254,7 @@ XOBJSTUB(MMovieXObj::m_Movie, 0)
 XOBJSTUBNR(MMovieXObj::m_dispose)
 
 void MMovieXObj::m_openMMovie(int nargs) {
-	g_lingo->printSTUBWithArglist("MMovieXObj::m_openMMovie", nargs);
+	g_lingo->printArgs("MMovieXObj::m_openMMovie", nargs);
 	if (nargs != 1) {
 		g_lingo->dropStack(nargs);
 		g_lingo->push(Datum(-1));
@@ -307,7 +307,7 @@ void MMovieXObj::m_openMMovie(int nargs) {
 }
 
 void MMovieXObj::m_closeMMovie(int nargs) {
-	g_lingo->printSTUBWithArglist("MMovieXObj::m_closeMMovie", nargs);
+	g_lingo->printArgs("MMovieXObj::m_closeMMovie", nargs);
 	if (nargs != 1) {
 		g_lingo->dropStack(nargs);
 		g_lingo->push(Datum(MMovieError::MMOVIE_INVALID_MOVIE_INDEX));
@@ -337,7 +337,7 @@ void MMovieXObj::m_closeMMovie(int nargs) {
 }
 
 void MMovieXObj::m_playSegment(int nargs) {
-	g_lingo->printSTUBWithArglist("MMovieXObj::m_playSegment", nargs);
+	g_lingo->printArgs("MMovieXObj::m_playSegment", nargs);
 	if (nargs != 5) {
 		g_lingo->dropStack(nargs);
 		g_lingo->push(Datum(MMovieError::MMOVIE_INVALID_SEGMENT_NAME));
@@ -375,7 +375,7 @@ void MMovieXObj::m_playSegment(int nargs) {
 }
 
 void MMovieXObj::m_playSegLoop(int nargs) {
-	g_lingo->printSTUBWithArglist("MMovieXObj::m_playSegLoop", nargs);
+	g_lingo->printArgs("MMovieXObj::m_playSegLoop", nargs);
 	if (nargs != 5) {
 		g_lingo->dropStack(nargs);
 		g_lingo->push(Datum(MMovieError::MMOVIE_INVALID_SEGMENT_NAME));
@@ -421,7 +421,7 @@ void MMovieXObj::m_idleSegment(int nargs) {
 }
 
 void MMovieXObj::m_stopSegment(int nargs) {
-	g_lingo->printSTUBWithArglist("MMovieXObj::m_stopSegment", nargs);
+	g_lingo->printArgs("MMovieXObj::m_stopSegment", nargs);
 	if (nargs != 0) {
 		g_lingo->dropStack(nargs);
 	}
@@ -431,7 +431,7 @@ void MMovieXObj::m_stopSegment(int nargs) {
 }
 
 void MMovieXObj::m_seekSegment(int nargs) {
-	g_lingo->printSTUBWithArglist("MMovieXObj::m_seekSegment", nargs);
+	g_lingo->printArgs("MMovieXObj::m_seekSegment", nargs);
 	if (nargs != 1) {
 		g_lingo->dropStack(nargs);
 		g_lingo->push(MMovieError::MMOVIE_INVALID_SEGMENT_NAME);
@@ -451,7 +451,7 @@ void MMovieXObj::m_seekSegment(int nargs) {
 XOBJSTUB(MMovieXObj::m_setSegmentTime, 0)
 
 void MMovieXObj::m_setDisplayBounds(int nargs) {
-	g_lingo->printSTUBWithArglist("MMovieXObj::m_setDisplayBounds", nargs);
+	g_lingo->printArgs("MMovieXObj::m_setDisplayBounds", nargs);
 	if (nargs != 4) {
 		warning("MMovieXObj::m_setDisplayBounds: expecting 4 arguments!");
 		g_lingo->dropStack(nargs);
@@ -472,7 +472,7 @@ XOBJSTUB(MMovieXObj::m_getMovieNormalWidth, 0)
 XOBJSTUB(MMovieXObj::m_getMovieNormalHeight, 0)
 
 void MMovieXObj::m_getSegCount(int nargs) {
-	g_lingo->printSTUBWithArglist("MMovieXObj::m_getSegCount", nargs);
+	g_lingo->printArgs("MMovieXObj::m_getSegCount", nargs);
 	if (nargs != 1) {
 		g_lingo->dropStack(nargs);
 		g_lingo->push(MMovieError::MMOVIE_INVALID_MOVIE_INDEX);
@@ -511,7 +511,7 @@ void MMovieXObj::m_getSegName(int nargs) {
 }
 
 void MMovieXObj::m_getMovieRate(int nargs) {
-	g_lingo->printSTUBWithArglist("MMovieXObj::m_getMovieRate", nargs);
+	g_lingo->printArgs("MMovieXObj::m_getMovieRate", nargs);
 	if (nargs != 0) {
 		g_lingo->dropStack(nargs);
 	}
@@ -520,7 +520,7 @@ void MMovieXObj::m_getMovieRate(int nargs) {
 }
 
 void MMovieXObj::m_setMovieRate(int nargs) {
-	g_lingo->printSTUBWithArglist("MMovieXObj::m_setMovieRate", nargs);
+	g_lingo->printArgs("MMovieXObj::m_setMovieRate", nargs);
 	if (nargs != 1) {
 		warning("MMovieXObj::m_setMovieRate: expecting 4 arguments");
 		g_lingo->dropStack(nargs);
@@ -536,7 +536,7 @@ XOBJSTUB(MMovieXObj::m_flushEvents, 0)
 XOBJSTUB(MMovieXObj::m_invalidateRect, 0)
 
 void MMovieXObj::m_readFile(int nargs) {
-	g_lingo->printSTUBWithArglist("MMovieXObj::m_readFile", nargs);
+	g_lingo->printArgs("MMovieXObj::m_readFile", nargs);
 	if (nargs != 2) {
 		warning("MMovieXObj::m_readFile(): expecting 2 argument");
 	}
@@ -597,7 +597,7 @@ void MMovieXObj::m_readFile(int nargs) {
 }
 
 void MMovieXObj::m_writeFile(int nargs) {
-	g_lingo->printSTUBWithArglist("MMovieXObj::m_writeFile", nargs);
+	g_lingo->printArgs("MMovieXObj::m_writeFile", nargs);
 	if (nargs != 3) {
 		warning("MMovieXObj::m_writeFile(): expecting 3 arguments");
 	}
diff --git a/engines/director/lingo/xlibs/remixxcmd.cpp b/engines/director/lingo/xlibs/remixxcmd.cpp
index 21e51e112fd..e84a41adbeb 100644
--- a/engines/director/lingo/xlibs/remixxcmd.cpp
+++ b/engines/director/lingo/xlibs/remixxcmd.cpp
@@ -292,7 +292,7 @@ void RemixXCMDState::interruptCheck() {
 }
 
 void RemixXCMD::m_Remix(int nargs) {
-	g_lingo->printSTUBWithArglist("RemixXCMD::m_Remix", nargs);
+	g_lingo->printArgs("RemixXCMD::m_Remix", nargs);
 	Datum result;
 	if (nargs != 1) {
 		result = Datum("Wrong number of params");


Commit: cabbd1288bfbe9058d04d93519484f9923ea320a
    https://github.com/scummvm/scummvm/commit/cabbd1288bfbe9058d04d93519484f9923ea320a
Author: Scott Percival (code at moral.net.au)
Date: 2024-05-12T22:00:19+02:00

Commit Message:
DIRECTOR: XOBJ: Add new UnitTest methods

Changed paths:
    engines/director/lingo/xlibs/unittest.cpp
    engines/director/lingo/xlibs/unittest.h


diff --git a/engines/director/lingo/xlibs/unittest.cpp b/engines/director/lingo/xlibs/unittest.cpp
index 766494267b0..718008fc41a 100644
--- a/engines/director/lingo/xlibs/unittest.cpp
+++ b/engines/director/lingo/xlibs/unittest.cpp
@@ -26,6 +26,7 @@
  *
  *************************************/
 
+#include "common/events.h"
 #include "director/director.h"
 #include "director/archive.h"
 #include "director/lingo/lingo.h"
@@ -47,10 +48,26 @@ const char *UnitTestXObj::fileNames[] = {
 	"UnitTest",
 	0
 };
+/*
+-- ScummVM UnitTest XObject.
+UnitTest
+I      mNew                     --Creates a new instance of the XObject
+X      mDispose                 --Disposes of XObject instance
+I      mIsRealDirector          --Returns 1 for real Director, 0 for ScummVM
+IS     mScreenshot, path        --Copy contents of stage window to file
+III    mMoveMouse, x, y         --Move the mouse pointer to window position (x, y)
+I      mLeftMouseDown           --Press the LMB
+I      mLeftMouseUp             --Release the LMB
+ */
 
 static MethodProto xlibMethods[] = {
-	{ "new",				UnitTestXObj::m_new,				 0, 0,	400 },	// D4
-	{ "screenshot",			UnitTestXObj::m_screenshot,			 1, 1,  400 },	// D4
+	{ "new",				UnitTestXObj::m_new,				0, 0,	400 },	// D4
+	{ "dispose",			UnitTestXObj::m_dispose,			0, 0,	400 },	// D4
+	{ "isRealDirector",		UnitTestXObj::m_isRealDirector,		0, 0,	400 },	// D4
+	{ "screenshot",			UnitTestXObj::m_screenshot,			1, 1,	400 },	// D4
+	{ "moveMouse",			UnitTestXObj::m_moveMouse,			2, 2,	400 },	// D4
+	{ "leftMouseDown",		UnitTestXObj::m_leftMouseDown,		0, 0,	400 },	// D4
+	{ "leftMouseUp",		UnitTestXObj::m_leftMouseUp,		0, 0,	400 },	// D4
 	{ nullptr, nullptr, 0, 0, 0 }
 };
 
@@ -77,6 +94,13 @@ void UnitTestXObj::m_new(int nargs) {
 	g_lingo->push(g_lingo->_state->me);
 }
 
+void UnitTestXObj::m_dispose(int nargs) {
+}
+
+void UnitTestXObj::m_isRealDirector(int nargs) {
+	g_lingo->push(0);
+}
+
 void UnitTestXObj::m_screenshot(int nargs) {
 	Common::String filenameBase = g_director->getCurrentMovie()->getArchive()->getFileName();
 	if (filenameBase.hasSuffixIgnoreCase(".dir"))
@@ -137,4 +161,52 @@ void UnitTestXObj::m_screenshot(int nargs) {
 	delete stream;
 }
 
+void UnitTestXObj::m_moveMouse(int nargs) {
+	if (nargs != 2) {
+		warning("UnitTestXObj::m_moveMouse: expected 2 arguments");
+		g_lingo->dropStack(nargs);
+		g_lingo->push(0);
+		return;
+	}
+	UnitTestXObject *me = static_cast<UnitTestXObject *>(g_lingo->_state->me.u.obj);
+	int16 y = (int16)g_lingo->pop().asInt();
+	int16 x = (int16)g_lingo->pop().asInt();
+	Common::Event ev;
+	ev.type = Common::EVENT_MOUSEMOVE;
+	ev.mouse = Common::Point(x, y);
+	me->_mousePos = ev.mouse;
+	g_director->getCurrentMovie()->processEvent(ev);
+	g_lingo->push(0);
+}
+
+void UnitTestXObj::m_leftMouseDown(int nargs) {
+	if (nargs != 0) {
+		warning("UnitTestXObj::m_leftMouseDown: expected 0 arguments");
+		g_lingo->dropStack(nargs);
+		g_lingo->push(0);
+		return;
+	}
+	UnitTestXObject *me = static_cast<UnitTestXObject *>(g_lingo->_state->me.u.obj);
+	Common::Event ev;
+	ev.type = Common::EVENT_LBUTTONDOWN;
+	ev.mouse = me->_mousePos;
+	g_director->getCurrentMovie()->processEvent(ev);
+	g_lingo->push(0);
+}
+
+void UnitTestXObj::m_leftMouseUp(int nargs) {
+	if (nargs != 0) {
+		warning("UnitTestXObj::m_leftMouseDown: expected 0 arguments");
+		g_lingo->dropStack(nargs);
+		g_lingo->push(0);
+		return;
+	}
+	UnitTestXObject *me = static_cast<UnitTestXObject *>(g_lingo->_state->me.u.obj);
+	Common::Event ev;
+	ev.type = Common::EVENT_LBUTTONUP;
+	ev.mouse = me->_mousePos;
+	g_director->getCurrentMovie()->processEvent(ev);
+	g_lingo->push(0);
+}
+
 } // End of namespace Director
diff --git a/engines/director/lingo/xlibs/unittest.h b/engines/director/lingo/xlibs/unittest.h
index 903d9be332d..e848476337d 100644
--- a/engines/director/lingo/xlibs/unittest.h
+++ b/engines/director/lingo/xlibs/unittest.h
@@ -27,6 +27,7 @@ namespace Director {
 class UnitTestXObject : public Object<UnitTestXObject> {
 public:
 	UnitTestXObject(ObjectType objType);
+	Common::Point _mousePos;
 };
 
 namespace UnitTestXObj {
@@ -38,7 +39,12 @@ void open(ObjectType type, const Common::Path &path);
 void close(ObjectType type);
 
 void m_new(int nargs);
+void m_dispose(int nargs);
+void m_isRealDirector(int nargs);
 void m_screenshot(int nargs);
+void m_moveMouse(int nargs);
+void m_leftMouseDown(int nargs);
+void m_leftMouseUp(int nargs);
 
 } // End of namespace UnitTest
 


Commit: e4b90dd23417ec00fd2483870b3817947e34d5bd
    https://github.com/scummvm/scummvm/commit/e4b90dd23417ec00fd2483870b3817947e34d5bd
Author: Scott Percival (code at moral.net.au)
Date: 2024-05-12T22:00:19+02:00

Commit Message:
DIRECTOR: Use mouse coordinates from individual events

Taking coordinates directly from the event manager could introduce
subtle timing problems, and makes it near-impossible to inject fake
events into the pipeline for testing.

Changed paths:
    engines/director/events.cpp


diff --git a/engines/director/events.cpp b/engines/director/events.cpp
index 9914fdc4859..f6fc129ef2c 100644
--- a/engines/director/events.cpp
+++ b/engines/director/events.cpp
@@ -121,7 +121,7 @@ bool Movie::processEvent(Common::Event &event) {
 
 	switch (event.type) {
 	case Common::EVENT_MOUSEMOVE:
-		pos = _window->getMousePos();
+		pos = event.mouse;
 
 		_lastEventTime = g_director->getMacTicks();
 		_lastRollTime =	 _lastEventTime;
@@ -158,7 +158,7 @@ bool Movie::processEvent(Common::Event &event) {
 
 		if (_currentDraggedChannel) {
 			if (_currentDraggedChannel->_sprite->_moveable) {
-				pos = _draggingSpriteOffset + _window->getMousePos();
+				pos = _draggingSpriteOffset + event.mouse;
 				if (!_currentDraggedChannel->_sprite->_trails) {
 					g_director->getCurrentMovie()->getWindow()->addDirtyRect(_currentDraggedChannel->getBbox());
 				}
@@ -173,11 +173,12 @@ bool Movie::processEvent(Common::Event &event) {
 
 	case Common::EVENT_LBUTTONDOWN:
 	case Common::EVENT_RBUTTONDOWN:
+		pos = event.mouse;
 		if (sc->_waitForClick) {
 			sc->_waitForClick = false;
-			sc->renderCursor(_window->getMousePos(), true);
+			sc->renderCursor(pos, true);
 		} else {
-			pos = _window->getMousePos();
+			pos = event.mouse;
 
 			// D3 doesn't have both mouse up and down.
 			// But we still want to know if the mouse is down for press effects.
@@ -187,7 +188,7 @@ bool Movie::processEvent(Common::Event &event) {
 			else
 				spriteId = sc->getMouseSpriteIDFromPos(pos);
 
-			_currentActiveSpriteId = sc->getActiveSpriteIDFromPos(pos);
+			_currentActiveSpriteId = sc->getActiveSpriteIDFromPos(pos); // the clickOn
 			_currentMouseSpriteId = sc->getMouseSpriteIDFromPos(pos);
 			_currentMouseDownCastID = sc->_channels[spriteId]->_sprite->_castId;
 
@@ -217,7 +218,7 @@ bool Movie::processEvent(Common::Event &event) {
 			queueUserEvent(kEventMouseDown, spriteId);
 
 			if (sc->_channels[spriteId]->_sprite->_moveable) {
-				_draggingSpriteOffset = sc->_channels[spriteId]->_currentPoint - _window->getMousePos();
+				_draggingSpriteOffset = sc->_channels[spriteId]->_currentPoint - pos;
 				_currentDraggedChannel = sc->_channels[spriteId];
 			}
 		}
@@ -226,7 +227,7 @@ bool Movie::processEvent(Common::Event &event) {
 
 	case Common::EVENT_LBUTTONUP:
 	case Common::EVENT_RBUTTONUP:
-		pos = _window->getMousePos();
+		pos = event.mouse;
 
 		if (g_director->getVersion() < 400)
 			spriteId = _currentActiveSpriteId;


Commit: 6a0fb37616c802770143c7f42f2264491057a63d
    https://github.com/scummvm/scummvm/commit/6a0fb37616c802770143c7f42f2264491057a63d
Author: Scott Percival (code at moral.net.au)
Date: 2024-05-12T22:00:19+02:00

Commit Message:
DIRECTOR: Allow only one input event to execute at a time

If you trigger e.g. a mouseDown and a mouseUp event, the mouseUp event
should not be run until the mouseDown event has returned.

Fixes director-tests/D4-unit/T_EVNT03.DIR

Changed paths:
    engines/director/events.cpp
    engines/director/lingo/lingo-builtins.cpp
    engines/director/lingo/lingo-events.cpp
    engines/director/lingo/lingo.cpp
    engines/director/lingo/lingo.h
    engines/director/movie.h
    engines/director/score.cpp
    engines/director/window.h


diff --git a/engines/director/events.cpp b/engines/director/events.cpp
index f6fc129ef2c..2c6151b7127 100644
--- a/engines/director/events.cpp
+++ b/engines/director/events.cpp
@@ -215,7 +215,7 @@ bool Movie::processEvent(Common::Event &event) {
 				_lastTimeOut = _lastEventTime;
 
 			debugC(3, kDebugEvents, "Movie::processEvent(): Button Down @(%d, %d), movie '%s', sprite id: %d", pos.x, pos.y, _macName.c_str(), spriteId);
-			queueUserEvent(kEventMouseDown, spriteId);
+			queueInputEvent(kEventMouseDown, spriteId);
 
 			if (sc->_channels[spriteId]->_sprite->_moveable) {
 				_draggingSpriteOffset = sc->_channels[spriteId]->_currentPoint - pos;
@@ -256,7 +256,7 @@ bool Movie::processEvent(Common::Event &event) {
 				cast->_hilite = !cast->_hilite;
 		}
 
-		queueUserEvent(kEventMouseUp, spriteId);
+		queueInputEvent(kEventMouseUp, spriteId);
 		sc->renderCursor(pos);
 
 		_currentHiliteChannelId = 0;
@@ -284,7 +284,7 @@ bool Movie::processEvent(Common::Event &event) {
 		if (_timeOutKeyDown)
 			_lastTimeOut = _lastEventTime;
 
-		queueUserEvent(kEventKeyDown);
+		queueInputEvent(kEventKeyDown);
 		g_director->loadSlowdownCooloff();
 		return true;
 
diff --git a/engines/director/lingo/lingo-builtins.cpp b/engines/director/lingo/lingo-builtins.cpp
index 812bfb30df9..31ecec462e2 100644
--- a/engines/director/lingo/lingo-builtins.cpp
+++ b/engines/director/lingo/lingo-builtins.cpp
@@ -2192,7 +2192,7 @@ void LB::b_importFileInto(int nargs) {
 }
 
 void menuCommandsCallback(int action, Common::String &text, void *data) {
-	g_director->getCurrentMovie()->queueUserEvent(kEventMenuCallback, action);
+	g_director->getCurrentMovie()->queueInputEvent(kEventMenuCallback, action);
 }
 
 void LB::b_installMenu(int nargs) {
diff --git a/engines/director/lingo/lingo-events.cpp b/engines/director/lingo/lingo-events.cpp
index a13737f316b..36a5a8d0716 100644
--- a/engines/director/lingo/lingo-events.cpp
+++ b/engines/director/lingo/lingo-events.cpp
@@ -337,18 +337,22 @@ void Movie::queueEvent(Common::Queue<LingoEvent> &queue, LEvent event, int targe
 	}
 }
 
-void Movie::queueUserEvent(LEvent event, int targetId) {
-	queueEvent(_userEventQueue, event, targetId);
+void Movie::queueInputEvent(LEvent event, int targetId) {
+	queueEvent(_inputEventQueue, event, targetId);
 }
 
 void Movie::processEvent(LEvent event, int targetId) {
 	Common::Queue<LingoEvent> queue;
 	queueEvent(queue, event, targetId);
 	_vm->setCurrentWindow(this->getWindow());
-	_lingo->processEvents(queue);
+	_lingo->processEvents(queue, false);
 }
 
-void Lingo::processEvents(Common::Queue<LingoEvent> &queue) {
+void Lingo::processEvents(Common::Queue<LingoEvent> &queue, bool isInputEvent) {
+	if (isInputEvent && _currentInputEvent.type != VOIDSYM) {
+		// only one input event should be in flight at a time.
+		return;
+	}
 	int lastEventId = -1;
 	Movie *movie = _vm->getCurrentMovie();
 	Score *sc = movie->getScore();
@@ -367,12 +371,20 @@ void Lingo::processEvents(Common::Queue<LingoEvent> &queue) {
 		}
 
 		_passEvent = el.passByDefault;
-		processEvent(el.event, el.scriptType, el.scriptId, el.channelId);
+
+		bool completed = processEvent(el.event, el.scriptType, el.scriptId, el.channelId);
+
+		if (isInputEvent && !completed) {
+			debugC(5, kDebugEvents, "Lingo::processEvents: context frozen on an input event, stopping");
+			LingoState *state = g_director->getCurrentWindow()->getLastFrozenLingoState();
+			_currentInputEvent = state->callstack.front()->sp;
+			break;
+		}
 		lastEventId = el.eventId;
 	}
 }
 
-void Lingo::processEvent(LEvent event, ScriptType st, CastMemberID scriptId, int channelId) {
+bool Lingo::processEvent(LEvent event, ScriptType st, CastMemberID scriptId, int channelId) {
 	_currentChannelId = channelId;
 
 	if (!_eventHandlerTypes.contains(event))
@@ -384,10 +396,11 @@ void Lingo::processEvent(LEvent event, ScriptType st, CastMemberID scriptId, int
 		debugC(1, kDebugEvents, "Lingo::processEvent(%s, %s, %s): executing event handler", _eventHandlerTypes[event], scriptType2str(st), scriptId.asString().c_str());
 		g_debugger->eventHook(event);
 		LC::call(script->_eventHandlers[event], 0, false);
-		execute();
+		return execute();
 	} else {
 		debugC(9, kDebugEvents, "Lingo::processEvent(%s, %s, %s): no handler", _eventHandlerTypes[event], scriptType2str(st), scriptId.asString().c_str());
 	}
+	return true;
 }
 
 } // End of namespace Director
diff --git a/engines/director/lingo/lingo.cpp b/engines/director/lingo/lingo.cpp
index 9507c45c0f9..e8b501ad5bb 100644
--- a/engines/director/lingo/lingo.cpp
+++ b/engines/director/lingo/lingo.cpp
@@ -107,6 +107,10 @@ Symbol& Symbol::operator=(const Symbol &s) {
 	return *this;
 }
 
+bool Symbol::operator==(Symbol &s) const {
+	return ctx == s.ctx && (*name == *s.name);
+}
+
 void Symbol::reset() {
 	*refCount -= 1;
 	// Coverity thinks that we always free memory, as it assumes
@@ -606,7 +610,7 @@ Common::String Lingo::formatFunctionBody(Symbol &sym) {
 	return result;
 }
 
-void Lingo::execute() {
+bool Lingo::execute() {
 	uint localCounter = 0;
 
 	while (!_abort && !_freezeState && _state->script && (*_state->script)[_state->pc] != STOP) {
@@ -670,6 +674,7 @@ void Lingo::execute() {
 		}
 	}
 
+	bool result = !_freezeState;
 	if (_freezeState) {
 		debugC(5, kDebugLingoExec, "Lingo::execute(): Context is frozen, pausing execution");
 		freezeState();
@@ -683,6 +688,8 @@ void Lingo::execute() {
 	_freezeState = false;
 
 	g_debugger->stepHook();
+	// return true if execution finished, false if the context froze for later
+	return result;
 }
 
 void Lingo::executeScript(ScriptType type, CastMemberID id) {
diff --git a/engines/director/lingo/lingo.h b/engines/director/lingo/lingo.h
index 7f3f8449fc2..8fa885ae381 100644
--- a/engines/director/lingo/lingo.h
+++ b/engines/director/lingo/lingo.h
@@ -102,6 +102,7 @@ struct Symbol {	/* symbol table entry */
 	Symbol();
 	Symbol(const Symbol &s);
 	Symbol& operator=(const Symbol &s);
+	bool operator==(Symbol &s) const;
 	void reset();
 	~Symbol();
 };
@@ -373,16 +374,16 @@ public:
 	// lingo-events.cpp
 private:
 	void initEventHandlerTypes();
-	void processEvent(LEvent event, ScriptType st, CastMemberID scriptId, int channelId = -1);
+	bool processEvent(LEvent event, ScriptType st, CastMemberID scriptId, int channelId = -1);
 
 public:
 	ScriptType event2script(LEvent ev);
 	Symbol getHandler(const Common::String &name);
 
-	void processEvents(Common::Queue<LingoEvent> &queue);
+	void processEvents(Common::Queue<LingoEvent> &queue, bool isInputEvent);
 
 public:
-	void execute();
+	bool execute();
 	void switchStateFromWindow();
 	void freezeState();
 	void pushContext(const Symbol funcSym, bool allowRetVal, Datum defaultRetVal, int paramCount);
@@ -532,6 +533,7 @@ public:
 	Datum _perFrameHook;
 
 	Datum _windowList;
+	Symbol _currentInputEvent;
 
 public:
 	void executeImmediateScripts(Frame *frame);
diff --git a/engines/director/movie.h b/engines/director/movie.h
index ed63c376e98..b6ae21121d1 100644
--- a/engines/director/movie.h
+++ b/engines/director/movie.h
@@ -131,7 +131,7 @@ public:
 	// lingo/lingo-events.cpp
 	void setPrimaryEventHandler(LEvent event, const Common::String &code);
 	void processEvent(LEvent event, int targetId = 0);
-	void queueUserEvent(LEvent event, int targetId = 0);
+	void queueInputEvent(LEvent event, int targetId = 0);
 
 private:
 	void loadFileInfo(Common::SeekableReadStreamEndian &stream);
@@ -168,7 +168,7 @@ public:
 	bool _videoPlayback;
 
 	int _nextEventId;
-	Common::Queue<LingoEvent> _userEventQueue;
+	Common::Queue<LingoEvent> _inputEventQueue;
 
 	unsigned char _key;
 	int _keyCode;
diff --git a/engines/director/score.cpp b/engines/director/score.cpp
index 3dbe4932b8a..154689350af 100644
--- a/engines/director/score.cpp
+++ b/engines/director/score.cpp
@@ -132,11 +132,16 @@ bool Score::processFrozenScripts() {
 	// to completion.
 	while (uint32 count = _window->frozenLingoStateCount()) {
 		_window->thawLingoState();
+		Symbol currentScript = _window->getLingoState()->callstack.front()->sp;
 		g_lingo->switchStateFromWindow();
-		g_lingo->execute();
-		if (_window->frozenLingoStateCount() >= count) {
+		bool completed = g_lingo->execute();
+		if (!completed || _window->frozenLingoStateCount() >= count) {
 			debugC(3, kDebugLingoExec, "Score::processFrozenScripts(): State froze again mid-thaw, interrupting");
 			return false;
+		} else if (currentScript == g_lingo->_currentInputEvent) {
+			// script that just completed was the current input event, clear the flag
+			debugC(3, kDebugEvents, "Score::processFrozenScripts(): Input event completed");
+			g_lingo->_currentInputEvent = Symbol();
 		}
 	}
 	return true;
@@ -301,9 +306,10 @@ void Score::step() {
 	if (_playState == kPlayStopped)
 		return;
 
-	if (!_movie->_userEventQueue.empty()) {
-		_lingo->processEvents(_movie->_userEventQueue);
-	} else if (_vm->getVersion() >= 300 && !_window->_newMovieStarted && _playState != kPlayStopped) {
+	if (!_movie->_inputEventQueue.empty()) {
+		_lingo->processEvents(_movie->_inputEventQueue, true);
+	}
+	if (_vm->getVersion() >= 300 && !_window->_newMovieStarted && _playState != kPlayStopped) {
 		_movie->processEvent(kEventIdle);
 	}
 
diff --git a/engines/director/window.h b/engines/director/window.h
index 0712edabe25..8e5f609f4ff 100644
--- a/engines/director/window.h
+++ b/engines/director/window.h
@@ -161,6 +161,7 @@ public:
 	uint32 frozenLingoStateCount() { return _frozenLingoStates.size(); };
 	void freezeLingoState();
 	void thawLingoState();
+	LingoState *getLastFrozenLingoState() { return _frozenLingoStates.empty() ? nullptr : _frozenLingoStates[_frozenLingoStates.size() - 1]; }
 	int recursiveEnterFrameCount();
 
 	// events.cpp


Commit: df0ccad5dc46e9033a0752258376e128b4046705
    https://github.com/scummvm/scummvm/commit/df0ccad5dc46e9033a0752258376e128b4046705
Author: Scott Percival (code at moral.net.au)
Date: 2024-05-12T22:00:19+02:00

Commit Message:
DIRECTOR: Add buffer for injecting input events

Forces UnitTest to wait for the engine's event processing pipeline to
run after injecting events.

Changed paths:
    engines/director/director.h
    engines/director/events.cpp
    engines/director/lingo/xlibs/mmovie.cpp
    engines/director/lingo/xlibs/paco.cpp
    engines/director/lingo/xlibs/quicktime.cpp
    engines/director/lingo/xlibs/unittest.cpp
    engines/director/lingo/xlibs/xplayanim.cpp
    engines/director/tests.cpp


diff --git a/engines/director/director.h b/engines/director/director.h
index fc6e1a39f84..2498a57b7f4 100644
--- a/engines/director/director.h
+++ b/engines/director/director.h
@@ -229,9 +229,11 @@ public:
 	bool desktopEnabled();
 
 	// events.cpp
+	bool pollEvent(Common::Event &event);
 	bool processEvents(bool captureClick = false, bool skipWindowManager = false);
 	void processEventQUIT();
 	uint32 getMacTicks();
+	Common::Array<Common::Event> _injectedEvents;
 
 	// game-quirks.cpp
 	void gameQuirks(const char *target, Common::Platform platform);
diff --git a/engines/director/events.cpp b/engines/director/events.cpp
index 2c6151b7127..f4484601d3e 100644
--- a/engines/director/events.cpp
+++ b/engines/director/events.cpp
@@ -40,13 +40,22 @@ namespace Director {
 
 uint32 DirectorEngine::getMacTicks() { return (g_system->getMillis() * 60 / 1000.) - _tickBaseline; }
 
+bool DirectorEngine::pollEvent(Common::Event &event) {
+	// used by UnitTest XObject
+	if (!_injectedEvents.empty()) {
+		event = _injectedEvents.remove_at(0);
+		return true;
+	}
+	return g_system->getEventManager()->pollEvent(event);
+}
+
 bool DirectorEngine::processEvents(bool captureClick, bool skipWindowManager) {
 	debugC(9, kDebugEvents, "\n@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");
 	debugC(9, kDebugEvents, "@@@@   Processing events");
 	debugC(9, kDebugEvents, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n");
 
 	Common::Event event;
-	while (g_system->getEventManager()->pollEvent(event)) {
+	while (pollEvent(event)) {
 		if (skipWindowManager || !_wm->processEvent(event)) {
 			// We only want to handle these events if the event
 			// wasn't handled by the window manager.
diff --git a/engines/director/lingo/xlibs/mmovie.cpp b/engines/director/lingo/xlibs/mmovie.cpp
index 9030f13a0cc..8846890c58f 100644
--- a/engines/director/lingo/xlibs/mmovie.cpp
+++ b/engines/director/lingo/xlibs/mmovie.cpp
@@ -163,7 +163,7 @@ void MMovieXObject::updateScreenBlocking() {
 	while (_currentMovieIndex && _currentSegmentIndex) {
 		Common::Event event;
 		bool keepPlaying = true;
-		if (g_system->getEventManager()->pollEvent(event)) {
+		if (g_director->pollEvent(event)) {
 			switch (event.type) {
 			case Common::EVENT_QUIT:
 				g_director->processEventQUIT();
diff --git a/engines/director/lingo/xlibs/paco.cpp b/engines/director/lingo/xlibs/paco.cpp
index 67a19a8ad67..92ca52a6562 100644
--- a/engines/director/lingo/xlibs/paco.cpp
+++ b/engines/director/lingo/xlibs/paco.cpp
@@ -131,7 +131,7 @@ void callPacoPlay(const Common::String &cmd) {
 		video->start();
 		memcpy(videoPalette, video->getPalette(), 256 * 3);
 		while (!video->endOfVideo()) {
-			if (g_system->getEventManager()->pollEvent(event)) {
+			if (g_director->pollEvent(event)) {
 				switch (event.type) {
 					case Common::EVENT_QUIT:
 						g_director->processEventQUIT();
diff --git a/engines/director/lingo/xlibs/quicktime.cpp b/engines/director/lingo/xlibs/quicktime.cpp
index 91110b5960a..a0d4e8f7784 100644
--- a/engines/director/lingo/xlibs/quicktime.cpp
+++ b/engines/director/lingo/xlibs/quicktime.cpp
@@ -93,7 +93,7 @@ void Quicktime::m_playStage(int nargs) {
     bool keepPlaying = true;
     Common::Event event;
     while (!video->endOfVideo()) {
-        if (g_system->getEventManager()->pollEvent(event)) {
+        if (g_director->pollEvent(event)) {
             switch(event.type) {
                 case Common::EVENT_QUIT:
                     g_director->processEventQUIT();
diff --git a/engines/director/lingo/xlibs/unittest.cpp b/engines/director/lingo/xlibs/unittest.cpp
index 718008fc41a..8e59f3c8136 100644
--- a/engines/director/lingo/xlibs/unittest.cpp
+++ b/engines/director/lingo/xlibs/unittest.cpp
@@ -175,7 +175,7 @@ void UnitTestXObj::m_moveMouse(int nargs) {
 	ev.type = Common::EVENT_MOUSEMOVE;
 	ev.mouse = Common::Point(x, y);
 	me->_mousePos = ev.mouse;
-	g_director->getCurrentMovie()->processEvent(ev);
+	g_director->_injectedEvents.push_back(ev);
 	g_lingo->push(0);
 }
 
@@ -190,7 +190,7 @@ void UnitTestXObj::m_leftMouseDown(int nargs) {
 	Common::Event ev;
 	ev.type = Common::EVENT_LBUTTONDOWN;
 	ev.mouse = me->_mousePos;
-	g_director->getCurrentMovie()->processEvent(ev);
+	g_director->_injectedEvents.push_back(ev);
 	g_lingo->push(0);
 }
 
@@ -205,7 +205,7 @@ void UnitTestXObj::m_leftMouseUp(int nargs) {
 	Common::Event ev;
 	ev.type = Common::EVENT_LBUTTONUP;
 	ev.mouse = me->_mousePos;
-	g_director->getCurrentMovie()->processEvent(ev);
+	g_director->_injectedEvents.push_back(ev);
 	g_lingo->push(0);
 }
 
diff --git a/engines/director/lingo/xlibs/xplayanim.cpp b/engines/director/lingo/xlibs/xplayanim.cpp
index f7159501243..213e963aa18 100644
--- a/engines/director/lingo/xlibs/xplayanim.cpp
+++ b/engines/director/lingo/xlibs/xplayanim.cpp
@@ -84,7 +84,7 @@ void XPlayAnim::b_xplayanim(int nargs) {
 	bool keepPlaying = true;
 	video->start();
 	while (!video->endOfVideo()) {
-		if (g_system->getEventManager()->pollEvent(event)) {
+		if (g_director->pollEvent(event)) {
 			switch(event.type) {
 				case Common::EVENT_QUIT:
 					g_director->processEventQUIT();
diff --git a/engines/director/tests.cpp b/engines/director/tests.cpp
index 6bfd9f5b127..543ccd61ca8 100644
--- a/engines/director/tests.cpp
+++ b/engines/director/tests.cpp
@@ -132,7 +132,7 @@ void Window::testFontScaling() {
 	Common::Event event;
 
 	while (true) {
-		if (g_system->getEventManager()->pollEvent(event))
+		if (g_director->pollEvent(event))
 			if (event.type == Common::EVENT_QUIT)
 				break;
 


Commit: aa15020b8d732830f46a2c2b4013bc5e09c0a213
    https://github.com/scummvm/scummvm/commit/aa15020b8d732830f46a2c2b4013bc5e09c0a213
Author: Scott Percival (code at moral.net.au)
Date: 2024-05-12T22:00:19+02:00

Commit Message:
DIRECTOR: Refactor mouse event handling

After creating a bunch of tests, it is clear that the choice of what
mouseDown/mouseUp handler in the hierarchy to use is determined just
before running it. This was a problem, as we were caching that
information up-front when processing the input events, and it is
entirely possible for e.g. a mouseDown handler to jump to another frame,
which would affect which subsequent mouseDown/mouseUp handlers to call.

Fixes director-tests/D4-unit/T_EVNT01 to T_EVNT11.

Changed paths:
    engines/director/events.cpp
    engines/director/lingo/lingo-builtins.cpp
    engines/director/lingo/lingo-events.cpp
    engines/director/lingo/lingo.h
    engines/director/movie.h
    engines/director/types.h


diff --git a/engines/director/events.cpp b/engines/director/events.cpp
index f4484601d3e..5eaac9a32be 100644
--- a/engines/director/events.cpp
+++ b/engines/director/events.cpp
@@ -189,33 +189,6 @@ bool Movie::processEvent(Common::Event &event) {
 		} else {
 			pos = event.mouse;
 
-			// D3 doesn't have both mouse up and down.
-			// But we still want to know if the mouse is down for press effects.
-			// Since we don't have mouse up and down before D3, then we use ActiveSprite
-			if (g_director->getVersion() < 400)
-				spriteId = sc->getActiveSpriteIDFromPos(pos);
-			else
-				spriteId = sc->getMouseSpriteIDFromPos(pos);
-
-			_currentActiveSpriteId = sc->getActiveSpriteIDFromPos(pos); // the clickOn
-			_currentMouseSpriteId = sc->getMouseSpriteIDFromPos(pos);
-			_currentMouseDownCastID = sc->_channels[spriteId]->_sprite->_castId;
-
-			if (!spriteId && _isBeepOn) {
-				g_lingo->func_beep(1);
-			}
-
-			if (spriteId > 0 && sc->_channels[spriteId]->_sprite->shouldHilite()) {
-				_currentHiliteChannelId = spriteId;
-				g_director->_wm->_hilitingWidget = true;
-				g_director->getCurrentWindow()->setDirty(true);
-				g_director->getCurrentWindow()->addDirtyRect(sc->_channels[_currentHiliteChannelId]->getBbox());
-			}
-
-			CastMember *cast = getCastMember(sc->_channels[spriteId]->_sprite->_castId);
-			if (cast && cast->_type == kCastButton)
-				_mouseDownWasInButton = true;
-
 			_lastEventTime = g_director->getMacTicks();
 			_lastClickTime2 = _lastClickTime;
 			_lastClickTime = _lastEventTime;
@@ -223,13 +196,8 @@ bool Movie::processEvent(Common::Event &event) {
 			if (_timeOutMouse)
 				_lastTimeOut = _lastEventTime;
 
-			debugC(3, kDebugEvents, "Movie::processEvent(): Button Down @(%d, %d), movie '%s', sprite id: %d", pos.x, pos.y, _macName.c_str(), spriteId);
-			queueInputEvent(kEventMouseDown, spriteId);
-
-			if (sc->_channels[spriteId]->_sprite->_moveable) {
-				_draggingSpriteOffset = sc->_channels[spriteId]->_currentPoint - pos;
-				_currentDraggedChannel = sc->_channels[spriteId];
-			}
+			debugC(3, kDebugEvents, "Movie::processEvent(): Button Down @(%d, %d), movie '%s'", pos.x, pos.y, _macName.c_str());
+			queueInputEvent(kEventMouseDown, 0, pos);
 		}
 
 		return true;
@@ -238,39 +206,10 @@ bool Movie::processEvent(Common::Event &event) {
 	case Common::EVENT_RBUTTONUP:
 		pos = event.mouse;
 
-		if (g_director->getVersion() < 400)
-			spriteId = _currentActiveSpriteId;
-		else
-			spriteId = _currentMouseSpriteId;
-
-		if (_currentHiliteChannelId && sc->_channels[_currentHiliteChannelId]) {
-			g_director->getCurrentWindow()->setDirty(true);
-			g_director->getCurrentWindow()->addDirtyRect(sc->_channels[_currentHiliteChannelId]->getBbox());
-		}
-
-		g_director->_wm->_hilitingWidget = false;
-
-		debugC(3, kDebugEvents, "Movie::processEvent(): Button Up @(%d, %d), movie '%s', sprite id: %d", pos.x, pos.y, _macName.c_str(), spriteId);
+		debugC(3, kDebugEvents, "Movie::processEvent(): Button Up @(%d, %d), movie '%s'", pos.x, pos.y, _macName.c_str());
 
-		_currentDraggedChannel = nullptr;
-
-		// If this is a button cast member, and the last mouse down event was in a button
-		// (any button), flip this button's hilite flag.
-		// Now you might think, "Wait, we don't flip this flag in the mouseDown event.
-		// And why any button??? This doesn't make any sense."
-		// No, it doesn't make sense, but it's what Director does.
-		if (_mouseDownWasInButton) {
-			CastMember *cast = getCastMember(sc->_channels[spriteId]->_sprite->_castId);
-			if (cast && cast->_type == kCastButton)
-				cast->_hilite = !cast->_hilite;
-		}
-
-		queueInputEvent(kEventMouseUp, spriteId);
+		queueInputEvent(kEventMouseUp, 0, pos);
 		sc->renderCursor(pos);
-
-		_currentHiliteChannelId = 0;
-		_mouseDownWasInButton = false;
-		g_director->loadSlowdownCooloff();
 		return true;
 
 	case Common::EVENT_KEYDOWN:
diff --git a/engines/director/lingo/lingo-builtins.cpp b/engines/director/lingo/lingo-builtins.cpp
index 31ecec462e2..4d958cfb2a1 100644
--- a/engines/director/lingo/lingo-builtins.cpp
+++ b/engines/director/lingo/lingo-builtins.cpp
@@ -1530,7 +1530,6 @@ void LB::b_halt(int nargs) {
 
 void LB::b_pass(int nargs) {
 	g_lingo->_passEvent = true;
-	warning("pass raised");
 }
 
 void LB::b_pause(int nargs) {
diff --git a/engines/director/lingo/lingo-events.cpp b/engines/director/lingo/lingo-events.cpp
index 36a5a8d0716..0166f024b18 100644
--- a/engines/director/lingo/lingo-events.cpp
+++ b/engines/director/lingo/lingo-events.cpp
@@ -25,10 +25,13 @@
 #include "director/lingo/lingo-code.h"
 #include "director/lingo/lingo-object.h"
 #include "director/cast.h"
-#include "director/movie.h"
+#include "director/castmember/castmember.h"
+#include "director/channel.h"
 #include "director/frame.h"
+#include "director/movie.h"
 #include "director/score.h"
 #include "director/sprite.h"
+#include "director/types.h"
 #include "director/window.h"
 
 namespace Director {
@@ -109,7 +112,106 @@ void Movie::setPrimaryEventHandler(LEvent event, const Common::String &code) {
 	mainArchive->replaceCode(code, kEventScript, event);
 }
 
-void Movie::queueSpriteEvent(Common::Queue<LingoEvent> &queue, LEvent event, int eventId, int spriteId) {
+void Movie::resolveScriptEvent(LingoEvent &event) {
+	// Resolve the script details of an event.
+	// This must be done at execution time, as it relies on
+	// e.g. the current frame, the current arrangement of sprites...
+	uint16 spriteId = 0;
+	if (event.mousePos != Common::Point(-1, -1)) {
+		// Fetch the sprite underneath the mouse cursor.
+
+		// D3 doesn't have both mouse up and down.
+		// But we still want to know if the mouse is down for press effects.
+		// Since we don't have mouse up and down before D3, then we use ActiveSprite
+		if (g_director->getVersion() < 400)
+			spriteId = _score->getActiveSpriteIDFromPos(event.mousePos);
+		else
+			spriteId = _score->getMouseSpriteIDFromPos(event.mousePos);
+		_currentActiveSpriteId = _score->getActiveSpriteIDFromPos(event.mousePos); // the clickOn
+		_currentMouseSpriteId = _score->getMouseSpriteIDFromPos(event.mousePos);
+	}
+	event.channelId = spriteId;
+
+	// mouseDown/mouseUp events will have one of each of the source types queued.
+	// run these steps at the very beginning (i.e. before the first source type).
+	if (event.eventHandlerSourceType == kPrimaryHandler) {
+		if (event.event == kEventMouseDown) {
+			if (!spriteId && _isBeepOn) {
+				g_lingo->func_beep(1);
+			}
+
+			if (spriteId > 0) {
+				if (_score->_channels[spriteId]->_sprite->shouldHilite()) {
+					_currentHiliteChannelId = spriteId;
+					g_director->_wm->_hilitingWidget = true;
+					g_director->getCurrentWindow()->setDirty(true);
+					g_director->getCurrentWindow()->addDirtyRect(_score->_channels[_currentHiliteChannelId]->getBbox());
+				}
+
+				CastMember *cast = getCastMember(_score->_channels[spriteId]->_sprite->_castId);
+				if (cast && cast->_type == kCastButton)
+					_mouseDownWasInButton = true;
+
+				if (_score->_channels[spriteId]->_sprite->_moveable) {
+					_draggingSpriteOffset = _score->_channels[spriteId]->_currentPoint - event.mousePos;
+					_currentDraggedChannel = _score->_channels[spriteId];
+				}
+
+				// In the case of clicking the mouse, it is possible for a mouseDown action to
+				// change the cast member underneath. on mouseUp should always load the cast
+				// script for the original cast member, not the new one.
+				_currentMouseDownCastID = _score->_channels[spriteId]->_sprite->_castId;
+
+			} else {
+				_currentHiliteChannelId = 0;
+				_mouseDownWasInButton = false;
+				_draggingSpriteOffset = Common::Point(0, 0);
+				_currentDraggedChannel = nullptr;
+				_currentMouseDownCastID = CastMemberID();
+			}
+
+		} else if (event.event == kEventMouseUp) {
+			if (_currentHiliteChannelId && _score->_channels[_currentHiliteChannelId]) {
+				g_director->getCurrentWindow()->setDirty(true);
+				g_director->getCurrentWindow()->addDirtyRect(_score->_channels[_currentHiliteChannelId]->getBbox());
+			}
+			g_director->_wm->_hilitingWidget = false;
+
+			_currentDraggedChannel = nullptr;
+
+			// If this is a button cast member, and the last mouse down event was in a button
+			// (any button), flip this button's hilite flag.
+			// Now you might think, "Wait, we don't flip this flag in the mouseDown event.
+			// And why any button??? This doesn't make any sense."
+			// No, it doesn't make sense, but it's what Director does.
+			if (_mouseDownWasInButton && spriteId) {
+				CastMember *cast = getCastMember(_score->_channels[spriteId]->_sprite->_castId);
+				if (cast && cast->_type == kCastButton)
+					cast->_hilite = !cast->_hilite;
+			}
+			_currentHiliteChannelId = 0;
+			_mouseDownWasInButton = false;
+			g_director->loadSlowdownCooloff();
+		}
+	}
+
+	switch (event.eventHandlerSourceType) {
+	case kPrimaryHandler:
+		// Run the primary event handler.
+		// Note that this isn't a "real" cast member ID, it's just the enum
+		// of the type of event, so it can be crammed into the script context
+		// index. kEventScript is a script type reserved for the primary event
+		// handlers (e.g. the mouseDownScript, the mouseUpScript), so there will
+		// be no collision with script cast members like ScoreScripts.
+		{
+			CastMemberID scriptId(event.event, DEFAULT_CAST_LIB);
+			if (getScriptContext(kEventScript, scriptId)) {
+				event.event = kEventGeneric;
+				event.scriptType = kEventScript;
+				event.scriptId = scriptId;
+			}
+		}
+		break;
 	/* When the mouseDown or mouseUp occurs over a sprite, the message
 	 * goes first to the sprite script, then to the script of the cast
 	 * member, to the frame script and finally to the movie scripts.
@@ -119,107 +221,137 @@ void Movie::queueSpriteEvent(Common::Queue<LingoEvent> &queue, LEvent event, int
 	 *
 	 * When more than one movie script [...]
 	 * [D4 docs] */
-
-	Frame *currentFrame = _score->_currentFrame;
-	assert(currentFrame != nullptr);
-	Sprite *sprite = _score->getSpriteById(spriteId);
-
-	// Sprite (score) script
-	if (sprite->_scriptId.member) {
-		ScriptContext *script = getScriptContext(kScoreScript, sprite->_scriptId);
-		if (script) {
-			if (script->_eventHandlers.contains(event)) {
-				// D4-style event handler
-				queue.push(LingoEvent(event, eventId, kScoreScript, sprite->_scriptId, false, spriteId));
-			} else if (script->_eventHandlers.contains(kEventGeneric)) {
-				// D3-style sprite script, not contained in a handler
-				// If sprite is immediate, its script is run on mouseDown, otherwise on mouseUp
-				if ((event == kEventMouseDown && sprite->_immediate) || (event == kEventMouseUp && !sprite->_immediate)) {
-					queue.push(LingoEvent(kEventGeneric, eventId, kScoreScript, sprite->_scriptId, false, spriteId));
+	case kSpriteHandler:
+		{
+			if (!spriteId)
+				return;
+			Frame *currentFrame = _score->_currentFrame;
+			assert(currentFrame != nullptr);
+			Sprite *sprite = _score->getSpriteById(spriteId);
+
+			// Sprite (score) script
+			if (sprite && sprite->_scriptId.member) {
+				ScriptContext *script = getScriptContext(kScoreScript, sprite->_scriptId);
+				if (script) {
+					if (script->_eventHandlers.contains(event.event)) {
+						// D4-style event handler
+						event.scriptType = kScoreScript;
+						event.scriptId = sprite->_scriptId;
+					} else if (script->_eventHandlers.contains(kEventGeneric)) {
+						// D3-style sprite script, not contained in a handler
+						// If sprite is immediate, its script is run on mouseDown, otherwise on mouseUp
+						if ((event.event == kEventMouseDown && sprite->_immediate) || (event.event == kEventMouseUp && !sprite->_immediate)) {
+							event.event = kEventGeneric;
+							event.scriptType = kScoreScript;
+							event.scriptId = sprite->_scriptId;
+						}
+						return; // FIXME: Do not execute the cast script if there is a D3-style sprite script
+					}
 				}
-				return; // Do not execute the cast script if there is a D3-style sprite script
 			}
 		}
-	}
-
-	// Cast script
-	CastMemberID targetCast = sprite->_castId;
-
-	// In the case of clicking the mouse, it is possible for a mouseDown action to
-	// change the cast member underneath. on mouseUp should try and load the cast
-	// script for the original cast member, not the new one.
-	if (event == kEventMouseUp) {
-		targetCast = _currentMouseDownCastID;
-	}
-	ScriptContext *script = getScriptContext(kCastScript, targetCast);
-	if (script && script->_eventHandlers.contains(event)) {
-		queue.push(LingoEvent(event, eventId, kCastScript, targetCast, false, spriteId));
-	}
-}
-
-void Movie::queueFrameEvent(Common::Queue<LingoEvent> &queue, LEvent event, int eventId) {
-	/* [in D4] the enterFrame, exitFrame, idle and timeout messages
-	 * are sent to a frame script and then a movie script.	If the
-	 * current frame has no frame script when the event occurs, the
-	 * message goes to movie scripts.
-	 * [p.81 of D4 docs]
-	 */
-
-	// if (event == kEventPrepareFrame || event == kEventIdle) {
-	// 	entity = score->getCurrentFrameNum();
-	// } else {
-
-	if (_score->_currentFrame == nullptr)
-		return;
-
-	CastMemberID scriptId = _score->_currentFrame->_mainChannels.actionId;
-	if (!scriptId.member)
-		return;
+		break;
+	case kCastHandler:
+		{
+			// Cast script
+			// A strange quirk; if we're in a mouseDown event, Director will test
+			// at runtime to find out whatever is under the mouse and use that.
+			// If we're in a mouseUp event, Director will use whatever was
+			// discovered -at the very beginning- of the mouseDown event chain.
+			// This means e.g. the cast member can be swapped out from underneath in
+			// the mouseDown sprite script and the event passed down, which
+			// will mean the old cast member cast script does not get a mouseDown
+			// call, but it -does- get a mouseUp call.
+			// A bit unhinged, but we have a test that proves Director does this,
+			// so we have to do it too.
+			CastMemberID targetCast = _currentMouseDownCastID;
+			if (event.event == kEventMouseDown) {
+				if (!spriteId)
+					return;
+				Sprite *sprite = _score->getSpriteById(spriteId);
+				targetCast = sprite->_castId;
+			}
 
-	ScriptContext *script = getScriptContext(kScoreScript, scriptId);
-	if (!script)
-		return;
+			ScriptContext *script = getScriptContext(kCastScript, targetCast);
+			if (script && script->_eventHandlers.contains(event.event)) {
+				event.scriptType = kCastScript;
+				event.scriptId = targetCast;
+			}
+		}
+		break;
+	case kFrameHandler:
+		{
+			/* [in D4] the enterFrame, exitFrame, idle and timeout messages
+			 * are sent to a frame script and then a movie script.	If the
+			 * current frame has no frame script when the event occurs, the
+			 * message goes to movie scripts.
+			 * [p.81 of D4 docs]
+			 */
+
+			if (_score->_currentFrame == nullptr)
+				return;
 
-	if (script->_eventHandlers.contains(event)) {
-		queue.push(LingoEvent(event, eventId, kScoreScript, scriptId, false, 0));
-	}
-	// Scopeless statements (ie one lined lingo commands) are executed at exitFrame
-	// A score script can have both scopeless and scoped lingo. (eg. porting from D3.1 to D4)
-	// In the event of both being specified in the ScoreScript, the scopeless handler is ignored.
+			CastMemberID scriptId = _score->_currentFrame->_mainChannels.actionId;
+			if (!scriptId.member)
+				return;
 
-	if (event == kEventExitFrame && script->_eventHandlers.contains(kEventGeneric) &&
-		!(script->_eventHandlers.contains(kEventExitFrame) || script->_eventHandlers.contains(kEventEnterFrame))) {
-		queue.push(LingoEvent(kEventGeneric, eventId, kScoreScript, scriptId, false, 0));
-	}
+			ScriptContext *script = getScriptContext(kScoreScript, scriptId);
+			if (!script)
+				return;
 
-}
+			if (script->_eventHandlers.contains(event.event)) {
+				event.scriptType = kScoreScript;
+				event.scriptId = scriptId;
+				return;
+			}
 
-void Movie::queueMovieEvent(Common::Queue<LingoEvent> &queue, LEvent event, int eventId) {
-	/* If more than one movie script handles the same message, Lingo
-	 * searches the movie scripts according to their order in the cast
-	 * window [p.81 of D4 docs]
-	 */
+			// Scopeless statements (ie one lined lingo commands) are executed at exitFrame
+			// A score script can have both scopeless and scoped lingo. (eg. porting from D3.1 to D4)
+			// In the event of both being specified in the ScoreScript, the scopeless handler is ignored.
 
-	// FIXME: shared cast movie scripts could come before main movie ones
-	LingoArchive *mainArchive = getMainLingoArch();
-	for (auto &it : mainArchive->scriptContexts[kMovieScript]) {
-		if (it._value->_eventHandlers.contains(event)) {
-			queue.push(LingoEvent(event, eventId, kMovieScript, CastMemberID(it._key, DEFAULT_CAST_LIB), false));
-			return;
+			if (event.event == kEventExitFrame && script->_eventHandlers.contains(kEventGeneric) &&
+				!(script->_eventHandlers.contains(kEventExitFrame) || script->_eventHandlers.contains(kEventEnterFrame))) {
+				event.event = kEventGeneric;
+				event.scriptType = kScoreScript;
+				event.scriptId = scriptId;
+			}
 		}
-	}
-	LingoArchive *sharedArchive = getSharedLingoArch();
-	if (sharedArchive) {
-		for (auto &it : sharedArchive->scriptContexts[kMovieScript]) {
-			if (it._value->_eventHandlers.contains(event)) {
-				queue.push(LingoEvent(event, eventId, kMovieScript, CastMemberID(it._key, DEFAULT_CAST_LIB), false));
-				return;
+		break;
+	case kMovieHandler:
+		{
+			/* If more than one movie script handles the same message, Lingo
+			 * searches the movie scripts according to their order in the cast
+			 * window [p.81 of D4 docs]
+			 */
+
+			// FIXME: shared cast movie scripts could come before main movie ones
+			// Movie scripts are fixed, so it's fine to look them up in advance.
+			LingoArchive *mainArchive = getMainLingoArch();
+			for (auto &it : mainArchive->scriptContexts[kMovieScript]) {
+				if (it._value->_eventHandlers.contains(event.event)) {
+					event.scriptType = kMovieScript;
+					event.scriptId = CastMemberID(it._key, DEFAULT_CAST_LIB);
+					return;
+				}
+			}
+			LingoArchive *sharedArchive = getSharedLingoArch();
+			if (sharedArchive) {
+				for (auto &it : sharedArchive->scriptContexts[kMovieScript]) {
+					if (it._value->_eventHandlers.contains(event.event)) {
+						event.scriptType = kMovieScript;
+						event.scriptId = CastMemberID(it._key, DEFAULT_CAST_LIB);
+						return;
+					}
+				}
 			}
 		}
+		break;
+	default:
+		break;
 	}
 }
 
-void Movie::queueEvent(Common::Queue<LingoEvent> &queue, LEvent event, int targetId) {
+void Movie::queueEvent(Common::Queue<LingoEvent> &queue, LEvent event, int targetId, Common::Point pos) {
 	int eventId = _nextEventId++;
 	if (_nextEventId < 0)
 		_nextEventId = 0;
@@ -244,17 +376,17 @@ void Movie::queueEvent(Common::Queue<LingoEvent> &queue, LEvent event, int targe
 	case kEventKeyDown:
 	case kEventTimeout:
 		{
-			CastMemberID scriptID = CastMemberID(event, DEFAULT_CAST_LIB);
-			if (getScriptContext(kEventScript, scriptID)) {
-				queue.push(LingoEvent(kEventGeneric, eventId, kEventScript, scriptID, true));
-			}
+			// Queue a call to the the primary event handler.
+			// As per above, by default this will pass through to any subsequent handlers,
+			// unless the script calls "dontPassEvent".
+			queue.push(LingoEvent(event, eventId, kPrimaryHandler, true, pos));
 		}
 		break;
 	case kEventMenuCallback:
 		{
 			CastMemberID scriptID = CastMemberID(targetId, DEFAULT_CAST_LIB);
 			if (getScriptContext(kEventScript, scriptID)) {
-				queue.push(LingoEvent(kEventGeneric, eventId, kEventScript, scriptID, true));
+				queue.push(LingoEvent(kEventGeneric, eventId, kEventScript, true, scriptID, pos));
 			}
 		}
 		break;
@@ -267,13 +399,12 @@ void Movie::queueEvent(Common::Queue<LingoEvent> &queue, LEvent event, int targe
 		switch(event) {
 		case kEventMouseUp:
 		case kEventMouseDown:
-			if (targetId) {
-				queueSpriteEvent(queue, event, eventId, targetId);
-			}
+			queue.push(LingoEvent(event, eventId, kSpriteHandler, false, pos));
+			queue.push(LingoEvent(event, eventId, kCastHandler, false, pos));
 			break;
 
 		case kEventExitFrame:
-			queueFrameEvent(queue, event, eventId);
+			queue.push(LingoEvent(event, eventId, kFrameHandler, false, pos));
 			break;
 
 		case kEventIdle:
@@ -281,7 +412,7 @@ void Movie::queueEvent(Common::Queue<LingoEvent> &queue, LEvent event, int targe
 		case kEventStartMovie:
 		case kEventStepMovie:
 		case kEventStopMovie:
-			queueMovieEvent(queue, event, eventId);
+			queue.push(LingoEvent(event, eventId, kMovieHandler, false, pos));
 			break;
 
 		// no-op; only handled by the primary event handler above
@@ -307,16 +438,15 @@ void Movie::queueEvent(Common::Queue<LingoEvent> &queue, LEvent event, int targe
 		case kEventMouseUp:
 		case kEventMouseDown:
 		case kEventBeginSprite:
-			if (targetId) {
-				queueSpriteEvent(queue, event, eventId, targetId);
-			}
+			queue.push(LingoEvent(event, eventId, kSpriteHandler, false, pos));
+			queue.push(LingoEvent(event, eventId, kCastHandler, false, pos));
 			// fall through
 
 		case kEventIdle:
 		case kEventEnterFrame:
 		case kEventExitFrame:
 		case kEventTimeout:
-			queueFrameEvent(queue, event, eventId);
+			queue.push(LingoEvent(event, eventId, kFrameHandler, false, pos));
 			// fall through
 
 		case kEventStartUp:
@@ -324,7 +454,7 @@ void Movie::queueEvent(Common::Queue<LingoEvent> &queue, LEvent event, int targe
 		case kEventStepMovie:
 		case kEventStopMovie:
 		case kEventPrepareMovie:
-			queueMovieEvent(queue, event, eventId);
+			queue.push(LingoEvent(event, eventId, kMovieHandler, false, pos));
 			break;
 
 		default:
@@ -337,8 +467,8 @@ void Movie::queueEvent(Common::Queue<LingoEvent> &queue, LEvent event, int targe
 	}
 }
 
-void Movie::queueInputEvent(LEvent event, int targetId) {
-	queueEvent(_inputEventQueue, event, targetId);
+void Movie::queueInputEvent(LEvent event, int targetId, Common::Point pos) {
+	queueEvent(_inputEventQueue, event, targetId, pos);
 }
 
 void Movie::processEvent(LEvent event, int targetId) {
@@ -363,6 +493,16 @@ void Lingo::processEvents(Common::Queue<LingoEvent> &queue, bool isInputEvent) {
 		if (sc->_playState == kPlayStopped && el.event != kEventStopMovie)
 			continue;
 
+		// fetch the sprite ID, script ID to call, etc if not present.
+		movie->resolveScriptEvent(el);
+
+		if (el.scriptType == kNoneScript) {
+			debugC(9, kDebugEvents, "Lingo::processEvents: no matching script for event (%s, %s, %s, %d), continuing",
+				_eventHandlerTypes[el.event], scriptType2str(el.scriptType), el.scriptId.asString().c_str(), el.channelId
+			);
+			continue;
+		}
+
 		if (lastEventId == el.eventId && !_passEvent) {
 			debugC(5, kDebugEvents, "Lingo::processEvents: swallowed event (%s, %s, %s, %d) because _passEvent was false",
 				_eventHandlerTypes[el.event], scriptType2str(el.scriptType), el.scriptId.asString().c_str(), el.channelId
@@ -372,6 +512,9 @@ void Lingo::processEvents(Common::Queue<LingoEvent> &queue, bool isInputEvent) {
 
 		_passEvent = el.passByDefault;
 
+		debugC(5, kDebugEvents, "Lingo::processEvents: starting event script (%s, %s, %s, %d)",
+			_eventHandlerTypes[el.event], scriptType2str(el.scriptType), el.scriptId.asString().c_str(), el.channelId
+		);
 		bool completed = processEvent(el.event, el.scriptType, el.scriptId, el.channelId);
 
 		if (isInputEvent && !completed) {
diff --git a/engines/director/lingo/lingo.h b/engines/director/lingo/lingo.h
index 8fa885ae381..ffe25efc3b3 100644
--- a/engines/director/lingo/lingo.h
+++ b/engines/director/lingo/lingo.h
@@ -261,18 +261,33 @@ struct CFrame {	/* proc/func call stack frame */
 struct LingoEvent {
 	LEvent event;
 	int eventId;
+	EventHandlerSourceType eventHandlerSourceType;
 	ScriptType scriptType;
-	CastMemberID scriptId;
 	bool passByDefault;
-	int channelId;
+	uint16 channelId;
+	CastMemberID scriptId;
+	Common::Point mousePos;
 
-	LingoEvent (LEvent e, int ei, ScriptType st, CastMemberID si, bool pass, int ci = -1) {
+	LingoEvent (LEvent e, int ei, ScriptType st, bool pass, CastMemberID si = CastMemberID(), Common::Point mp = Common::Point(-1, -1)) {
 		event = e;
 		eventId = ei;
+		eventHandlerSourceType = kNoneHandler;
 		scriptType = st;
+		passByDefault = pass;
+		channelId = 0;
 		scriptId = si;
+		mousePos = mp;
+	}
+
+	LingoEvent (LEvent e, int ei, EventHandlerSourceType ehst, bool pass, Common::Point mp = Common::Point(-1, -1)) {
+		event = e;
+		eventId = ei;
+		eventHandlerSourceType = ehst;
+		scriptType = kNoneScript;
 		passByDefault = pass;
-		channelId = ci;
+		channelId = 0;
+		scriptId = CastMemberID();
+		mousePos = mp;
 	}
 };
 
diff --git a/engines/director/movie.h b/engines/director/movie.h
index b6ae21121d1..9067d92fb8b 100644
--- a/engines/director/movie.h
+++ b/engines/director/movie.h
@@ -130,16 +130,15 @@ public:
 
 	// lingo/lingo-events.cpp
 	void setPrimaryEventHandler(LEvent event, const Common::String &code);
+	void resolveScriptEvent(LingoEvent &event);
 	void processEvent(LEvent event, int targetId = 0);
-	void queueInputEvent(LEvent event, int targetId = 0);
+	void queueInputEvent(LEvent event, int targetId = 0, Common::Point pos = Common::Point(-1, -1));
 
 private:
 	void loadFileInfo(Common::SeekableReadStreamEndian &stream);
 
-	void queueEvent(Common::Queue<LingoEvent> &queue, LEvent event, int targetId = 0);
+	void queueEvent(Common::Queue<LingoEvent> &queue, LEvent event, int targetId = 0, Common::Point pos = Common::Point(-1, -1));
 	void queueSpriteEvent(Common::Queue<LingoEvent> &queue, LEvent event, int eventId, int spriteId);
-	void queueFrameEvent(Common::Queue<LingoEvent> &queue, LEvent event, int eventId);
-	void queueMovieEvent(Common::Queue<LingoEvent> &queue, LEvent event, int eventId);
 
 public:
 	Archive *_movieArchive;
diff --git a/engines/director/types.h b/engines/director/types.h
index 791191f9067..fbdb4fd956a 100644
--- a/engines/director/types.h
+++ b/engines/director/types.h
@@ -71,6 +71,15 @@ enum ScriptType {
 	kMaxScriptType = 7	// Sync with types.cpp:28, array scriptTypes[]
 };
 
+enum EventHandlerSourceType {
+	kNoneHandler = 0,
+	kPrimaryHandler = 1,
+	kSpriteHandler = 2,
+	kCastHandler = 3,
+	kFrameHandler = 4,
+	kMovieHandler = 5
+};
+
 enum ScriptFlag {
 	kScriptFlagUnused		= (1 << 0x0),
 	kScriptFlagFuncsGlobal	= (1 << 0x1),


Commit: 8e84fa687e39b4f7cf0829cc8c6925895f856a43
    https://github.com/scummvm/scummvm/commit/8e84fa687e39b4f7cf0829cc8c6925895f856a43
Author: Scott Percival (code at moral.net.au)
Date: 2024-05-12T22:00:19+02:00

Commit Message:
DIRECTOR: LINGO: Handle arithmetic attempted on cast references

Fixes using the timezone switcher in Virtual Nightclub.

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


diff --git a/engines/director/lingo/lingo-code.cpp b/engines/director/lingo/lingo-code.cpp
index 695636d2226..12e58cd544a 100644
--- a/engines/director/lingo/lingo-code.cpp
+++ b/engines/director/lingo/lingo-code.cpp
@@ -706,6 +706,11 @@ Datum LC::mapBinaryOp(Datum (*mapFunc)(Datum &, Datum &), Datum &d1, Datum &d2)
 }
 
 Datum LC::addData(Datum &d1, Datum &d2) {
+	if (d1.type == CASTREF || d2.type == CASTREF) {
+		warning("LC::addData(): attempting to add a cast reference! This always produces 0, but might be a sign that an earlier part of the script has returned incorrect data.");
+		return Datum(0);
+	}
+
 	if (d1.isArray() || d2.isArray()) {
 		return LC::mapBinaryOp(LC::addData, d1, d2);
 	}
@@ -730,6 +735,11 @@ void LC::c_add() {
 }
 
 Datum LC::subData(Datum &d1, Datum &d2) {
+	if (d1.type == CASTREF || d2.type == CASTREF) {
+		warning("LC::subData(): attempting to subtract a cast reference! This always produces 0, but might be a sign that an earlier part of the script has returned incorrect data.");
+		return Datum(0);
+	}
+
 	if (d1.isArray() || d2.isArray()) {
 		return LC::mapBinaryOp(LC::subData, d1, d2);
 	}
@@ -754,6 +764,11 @@ void LC::c_sub() {
 }
 
 Datum LC::mulData(Datum &d1, Datum &d2) {
+	if (d1.type == CASTREF || d2.type == CASTREF) {
+		warning("LC::mulData(): attempting to multiply a cast reference! This always produces 0, but might be a sign that an earlier part of the script has returned incorrect data.");
+		return Datum(0);
+	}
+
 	if (d1.isArray() || d2.isArray()) {
 		return LC::mapBinaryOp(LC::mulData, d1, d2);
 	}
@@ -778,6 +793,11 @@ void LC::c_mul() {
 }
 
 Datum LC::divData(Datum &d1, Datum &d2) {
+	if (d1.type == CASTREF || d2.type == CASTREF) {
+		warning("LC::divData(): attempting to divide a cast reference! This always produces 0, but might be a sign that an earlier part of the script has returned incorrect data.");
+		return Datum(0);
+	}
+
 	if (d1.isArray() || d2.isArray()) {
 		return LC::mapBinaryOp(LC::divData, d1, d2);
 	}


Commit: 3bcc920a1e1d2e00236e0e87a5538c83d3bc0a20
    https://github.com/scummvm/scummvm/commit/3bcc920a1e1d2e00236e0e87a5538c83d3bc0a20
Author: Scott Percival (code at moral.net.au)
Date: 2024-05-12T22:00:19+02:00

Commit Message:
DIRECTOR: XOBJ: Have MMovie cache the last drawn frame

Changed paths:
    engines/director/lingo/xlibs/mmovie.cpp
    engines/director/lingo/xlibs/mmovie.h


diff --git a/engines/director/lingo/xlibs/mmovie.cpp b/engines/director/lingo/xlibs/mmovie.cpp
index 8846890c58f..238846f95b7 100644
--- a/engines/director/lingo/xlibs/mmovie.cpp
+++ b/engines/director/lingo/xlibs/mmovie.cpp
@@ -117,6 +117,10 @@ MMovieXObject::MMovieXObject(ObjectType ObjectType) :Object<MMovieXObject>("MMov
 	_objType = ObjectType;
 }
 
+MMovieXObject::~MMovieXObject() {
+	_lastFrame.free();
+}
+
 bool MMovieXObject::playSegment(int movieIndex, int segIndex, bool looping, bool restore, bool shiftAbort, bool abortOnClick, bool purge, bool async) {
 	if (_movies.contains(movieIndex)) {
 		MMovieFile &movie = _movies.getVal(movieIndex);
@@ -198,11 +202,12 @@ void MMovieXObject::updateScreen() {
 					debugC(8, kDebugXObj, "MMovieXObject: rendering movie %s (%d), time %d", movie._path.toString().c_str(), _currentMovieIndex, movie._video->getTime());
 					Graphics::Surface *temp1 = frame->scale(_bounds.width(), _bounds.height(), false);
 					Graphics::Surface *temp2 = temp1->convertTo(g_director->_pixelformat, movie._video->getPalette());
-					g_system->copyRectToScreen(temp2->getPixels(), temp2->pitch, _bounds.left, _bounds.top, _bounds.width(), _bounds.height());
+					_lastFrame.copyFrom(*temp2);
 					delete temp2;
 					delete temp1;
 				}
 			}
+			g_system->copyRectToScreen(_lastFrame.getPixels(), _lastFrame.pitch, _bounds.left, _bounds.top, _bounds.width(), _bounds.height());
 			// do a time check
 			uint32 endTime = Audio::Timestamp(0, seg._length + seg._start, movie._video->getTimeScale()).msecs();
 			debugC(8, kDebugXObj, "MMovieXObject::updateScreen(): time: %d, endTime: %d", movie._video->getTime(), endTime);
@@ -366,7 +371,7 @@ void MMovieXObj::m_playSegment(int nargs) {
 			}
 			int result = me->getTicks();
 			debugC(5, kDebugXObj, "MMovieXObj::m_playSegment: ticks: %d", result);
-			g_lingo->push(result);
+			g_lingo->push(0);
 			return;
 		}
 	}
@@ -401,7 +406,7 @@ void MMovieXObj::m_playSegLoop(int nargs) {
 			me->playSegment(it._key, segIndex, true, restore, shiftAbort, abortOnClick, purge, async);
 			int result = me->getTicks();
 			debugC(5, kDebugXObj, "MMovieXObj::m_playSegLoop: ticks: %d", result);
-			g_lingo->push(result);
+			g_lingo->push(0);
 			return;
 		}
 	}
@@ -465,6 +470,8 @@ void MMovieXObj::m_setDisplayBounds(int nargs) {
 	Datum top = g_lingo->pop();
 	Datum left = g_lingo->pop();
 	me->_bounds = Common::Rect((int16)left.asInt(), (int16)top.asInt(), (int16)right.asInt(), (int16)bottom.asInt());
+	me->_lastFrame.free();
+	me->_lastFrame.create(me->_bounds.width(), me->_bounds.height(), g_director->_pixelformat);
 	g_lingo->push(Datum(0));
 }
 
diff --git a/engines/director/lingo/xlibs/mmovie.h b/engines/director/lingo/xlibs/mmovie.h
index ae97dc081ee..467991e479b 100644
--- a/engines/director/lingo/xlibs/mmovie.h
+++ b/engines/director/lingo/xlibs/mmovie.h
@@ -62,6 +62,7 @@ struct MMovieFile {
 class MMovieXObject : public Object<MMovieXObject> {
 public:
 	MMovieXObject(ObjectType objType);
+	~MMovieXObject();
 
 	int _rate = 100;
 	Common::Rect _bounds;
@@ -78,6 +79,7 @@ public:
 	Common::HashMap<int, MMovieFile> _movies;
 	Common::HashMap<Common::String, int, Common::IgnoreCase_Hash, Common::IgnoreCase_EqualTo> _moviePathMap;
 
+	Graphics::Surface _lastFrame;
 
 	bool playSegment(int movieIndex, int segIndex, bool looping, bool restore, bool shiftAbort, bool abortOnClick, bool purge, bool async);
 	bool stopSegment();


Commit: 5cd47cec3856f07b971f9246b2c5b9d0c5c1b476
    https://github.com/scummvm/scummvm/commit/5cd47cec3856f07b971f9246b2c5b9d0c5c1b476
Author: Scott Percival (code at moral.net.au)
Date: 2024-05-12T22:00:19+02:00

Commit Message:
DIRECTOR: LINGO: Fix Datum collision with b_abs

The output of b_abs shared a refCount handle with the input, so e.g.
updating a variable in-place would result in no change.

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


diff --git a/engines/director/lingo/lingo-builtins.cpp b/engines/director/lingo/lingo-builtins.cpp
index 4d958cfb2a1..58d83562dde 100644
--- a/engines/director/lingo/lingo-builtins.cpp
+++ b/engines/director/lingo/lingo-builtins.cpp
@@ -356,13 +356,14 @@ void Lingo::drop(uint num) {
 ///////////////////
 void LB::b_abs(int nargs) {
 	Datum d = g_lingo->pop();
+	Datum res(0);
 
 	if (d.type == INT)
-		d.u.i = ABS(d.u.i);
+		res = Datum(ABS(d.u.i));
 	else if (d.type == FLOAT)
-		d.u.f = ABS(d.u.f);
+		res = Datum(ABS(d.u.f));
 
-	g_lingo->push(d);
+	g_lingo->push(res);
 }
 
 void LB::b_atan(int nargs) {


Commit: 60f1a24c81c98bbbc28a78a4190aa317e60d87ed
    https://github.com/scummvm/scummvm/commit/60f1a24c81c98bbbc28a78a4190aa317e60d87ed
Author: Scott Percival (code at moral.net.au)
Date: 2024-05-12T22:00:19+02:00

Commit Message:
DIRECTOR: LINGO: Add support for script objects to some PARRAY methods

In an undocumented use, you can access the property list of a script object
with these.

Fixes the Stella Artois label hunt minigame in Virtual Nightclub.

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


diff --git a/engines/director/lingo/lingo-builtins.cpp b/engines/director/lingo/lingo-builtins.cpp
index 58d83562dde..d5be3cd8824 100644
--- a/engines/director/lingo/lingo-builtins.cpp
+++ b/engines/director/lingo/lingo-builtins.cpp
@@ -687,8 +687,11 @@ void LB::b_count(int nargs) {
 	case PARRAY:
 		result.u.i = list.u.parr->arr.size();
 		break;
+	case OBJECT:
+		result.u.i = list.u.obj->getPropCount();
+		break;
 	default:
-		TYPECHECK2(list, ARRAY, PARRAY);
+		TYPECHECK3(list, ARRAY, PARRAY, OBJECT);
 	}
 
 	g_lingo->push(result);
@@ -822,8 +825,21 @@ void LB::b_getaProp(int nargs) {
 		g_lingo->push(d);
 		break;
 	}
+	case OBJECT:
+		{
+			if (prop.type != SYMBOL) {
+				g_lingo->lingoError("b_getaProp(): symbol expected");
+				return;
+			}
+			Datum d;
+			if (list.u.obj->hasProp(*prop.u.s))
+				d = list.u.obj->getProp(*prop.u.s);
+			g_lingo->push(d);
+		}
+		break;
 	default:
-		TYPECHECK2(list, ARRAY, PARRAY);
+		TYPECHECK3(list, ARRAY, PARRAY, OBJECT);
+		break;
 	}
 }
 
@@ -927,7 +943,6 @@ void LB::b_getPos(int nargs) {
 void LB::b_getProp(int nargs) {
 	Datum prop = g_lingo->pop();
 	Datum list = g_lingo->pop();
-	TYPECHECK2(list, ARRAY, PARRAY);
 
 	switch (list.type) {
 	case ARRAY:
@@ -944,7 +959,20 @@ void LB::b_getProp(int nargs) {
 		}
 		break;
 	}
+	case OBJECT:
+		{
+			if (prop.type != SYMBOL) {
+				g_lingo->lingoError("b_getProp(): symbol expected");
+				return;
+			}
+			Datum d;
+			if (list.u.obj->hasProp(*prop.u.s))
+				d = list.u.obj->getProp(*prop.u.s);
+			g_lingo->push(d);
+		}
+		break;
 	default:
+		TYPECHECK3(list, ARRAY, PARRAY, OBJECT);
 		break;
 	}
 }
@@ -953,10 +981,34 @@ void LB::b_getPropAt(int nargs) {
 	Datum indexD = g_lingo->pop();
 	Datum list = g_lingo->pop();
 	TYPECHECK2(indexD, INT, FLOAT);
-	TYPECHECK(list, PARRAY);
 	int index = indexD.asInt();
+	switch (list.type) {
+	case PARRAY:
+		{
+			if ((index <= 0) || (index > (int)list.u.parr->arr.size())) {
+				g_lingo->lingoError("b_getPropAt(): index out of range");
+				return;
+			}
+			g_lingo->push(list.u.parr->arr[index - 1].p);
+		}
+		break;
+	case OBJECT:
+		{
+			if ((index <= 0) || (index > (int)list.u.obj->getPropCount())) {
+				g_lingo->lingoError("b_getPropAt(): index out of range");
+				return;
+			}
+			Common::String key = list.u.obj->getPropAt(index);
+			Datum result(key);
+			result.type = SYMBOL;
+			g_lingo->push(result);
+		}
+		break;
+	default:
+		TYPECHECK2(list, PARRAY, OBJECT);
+		break;
+	}
 
-	g_lingo->push(list.u.parr->arr[index - 1].p);
 }
 
 void LB::b_list(int nargs) {
@@ -1067,6 +1119,15 @@ void LB::b_setaProp(int nargs) {
 		}
 		break;
 	}
+	case OBJECT:
+		{
+			if (prop.type != SYMBOL) {
+				g_lingo->lingoError("b_setaProp(): symbol expected");
+				return;
+			}
+			list.u.obj->setProp(*prop.u.s, value);
+		}
+		break;
 	default:
 		TYPECHECK2(list, ARRAY, PARRAY);
 	}
@@ -1108,13 +1169,32 @@ void LB::b_setProp(int nargs) {
 	Datum value = g_lingo->pop();
 	Datum prop = g_lingo->pop();
 	Datum list = g_lingo->pop();
-	TYPECHECK(list, PARRAY);
 
-	int index = LC::compareArrays(LC::eqData, list, prop, true).u.i;
-	if (index > 0) {
-		list.u.parr->arr[index - 1].v = value;
-	} else {
-		warning("b_setProp: Property not found");
+	switch (list.type) {
+	case PARRAY:
+		{
+			int index = LC::compareArrays(LC::eqData, list, prop, true).u.i;
+			if (index > 0) {
+				list.u.parr->arr[index - 1].v = value;
+			} else {
+				warning("b_setProp: Property not found");
+			}
+		}
+		break;
+	case OBJECT:
+		{
+			if (prop.type != SYMBOL) {
+				g_lingo->lingoError("b_setProp(): symbol expected");
+				return;
+			}
+			// unlike the PARRAY case, OBJECT seems to create
+			// new properties without throwing an error
+			list.u.obj->setProp(*prop.u.s, value);
+		}
+		break;
+	default:
+		TYPECHECK2(list, PARRAY, OBJECT);
+		break;
 	}
 }
 
diff --git a/engines/director/lingo/lingo-object.cpp b/engines/director/lingo/lingo-object.cpp
index 6cc942dca25..4efe7ce0715 100644
--- a/engines/director/lingo/lingo-object.cpp
+++ b/engines/director/lingo/lingo-object.cpp
@@ -508,6 +508,22 @@ Datum ScriptContext::getProp(const Common::String &propName) {
 	return _properties[propName]; // return new property
 }
 
+Common::String ScriptContext::getPropAt(uint32 index) {
+	// FIXME: Refactor the whole thing to have ordered keys
+	uint32 target = 1;
+	for (auto &it : _properties) {
+		if (target == index) {
+			return it._key;
+		}
+		target += 1;
+	}
+	return Common::String();
+}
+
+uint32 ScriptContext::getPropCount() {
+	return _properties.size();
+}
+
 bool ScriptContext::setProp(const Common::String &propName, const Datum &value) {
 	if (_disposed) {
 		error("Property '%s' accessed on disposed object <%s>", propName.c_str(), Datum(this).asString(true).c_str());
diff --git a/engines/director/lingo/lingo-object.h b/engines/director/lingo/lingo-object.h
index 1ab37829ccc..09446e6dc25 100644
--- a/engines/director/lingo/lingo-object.h
+++ b/engines/director/lingo/lingo-object.h
@@ -54,6 +54,8 @@ public:
 	virtual Symbol getMethod(const Common::String &methodName) = 0;
 	virtual bool hasProp(const Common::String &propName) = 0;
 	virtual Datum getProp(const Common::String &propName) = 0;
+	virtual Common::String getPropAt(uint32 index) = 0;
+	virtual uint32 getPropCount() = 0;
 	virtual bool setProp(const Common::String &propName, const Datum &value) = 0;
 	virtual bool hasField(int field) = 0;
 	virtual Datum getField(int field) = 0;
@@ -173,6 +175,12 @@ public:
 	Datum getProp(const Common::String &propName) override {
 		return Datum();
 	};
+	Common::String getPropAt(uint32 index) override {
+		return Common::String();
+	};
+	uint32 getPropCount() override {
+		return 0;
+	};
 	bool setProp(const Common::String &propName, const Datum &value) override {
 		return false;
 	};
@@ -227,6 +235,8 @@ public:
 	Symbol getMethod(const Common::String &methodName) override;
 	bool hasProp(const Common::String &propName) override;
 	Datum getProp(const Common::String &propName) override;
+	Common::String getPropAt(uint32 index) override;
+	uint32 getPropCount() override;
 	bool setProp(const Common::String &propName, const Datum &value) override;
 
 	Symbol define(const Common::String &name, ScriptData *code, Common::Array<Common::String> *argNames, Common::Array<Common::String> *varNames);


Commit: 428ad1ac189a2f32f6658a4ff3aee22adc1f229c
    https://github.com/scummvm/scummvm/commit/428ad1ac189a2f32f6658a4ff3aee22adc1f229c
Author: Scott Percival (code at moral.net.au)
Date: 2024-05-12T22:00:19+02:00

Commit Message:
DIRECTOR: Fix Cursor::readFromCast for D5

Changed paths:
    engines/director/cursor.cpp


diff --git a/engines/director/cursor.cpp b/engines/director/cursor.cpp
index 586489816e8..281a6b9e00b 100644
--- a/engines/director/cursor.cpp
+++ b/engines/director/cursor.cpp
@@ -82,7 +82,13 @@ void Cursor::readFromCast(Datum cursorCasts) {
 	_usePalette = false;
 	_keyColor = 3;
 
-	resetCursor(Graphics::kMacCursorCustom, true, Datum(cursorId.member));
+	Datum cursorRes;
+	if (g_director->getVersion() < 500) {
+		cursorRes = Datum(cursorId.member);
+	} else {
+		cursorRes = Datum((cursorId.castLib << 16) + cursorId.member);
+	}
+	resetCursor(Graphics::kMacCursorCustom, true, cursorRes);
 
 	BitmapCastMember *cursorBitmap = (BitmapCastMember *)cursorCast;
 	BitmapCastMember *maskBitmap = (BitmapCastMember *)maskCast;


Commit: 74efd8e6eabf3a8656e658b7d635cfc1df72f0b8
    https://github.com/scummvm/scummvm/commit/74efd8e6eabf3a8656e658b7d635cfc1df72f0b8
Author: Scott Percival (code at moral.net.au)
Date: 2024-05-12T22:00:19+02:00

Commit Message:
DIRECTOR: Fixes based on review feedback

Changed paths:
    engines/director/lingo/xlibs/mmovie.cpp
    engines/director/sprite.cpp


diff --git a/engines/director/lingo/xlibs/mmovie.cpp b/engines/director/lingo/xlibs/mmovie.cpp
index 238846f95b7..95645c72f0c 100644
--- a/engines/director/lingo/xlibs/mmovie.cpp
+++ b/engines/director/lingo/xlibs/mmovie.cpp
@@ -545,7 +545,10 @@ XOBJSTUB(MMovieXObj::m_invalidateRect, 0)
 void MMovieXObj::m_readFile(int nargs) {
 	g_lingo->printArgs("MMovieXObj::m_readFile", nargs);
 	if (nargs != 2) {
-		warning("MMovieXObj::m_readFile(): expecting 2 argument");
+		warning("MMovieXObj::m_readFile(): expecting 2 arguments");
+		g_lingo->dropStack(nargs);
+		g_lingo->push(Datum(""));
+		return;
 	}
 	Common::SaveFileManager *saves = g_system->getSavefileManager();
 	bool scramble = g_lingo->pop().asInt() != 0;
@@ -607,6 +610,9 @@ void MMovieXObj::m_writeFile(int nargs) {
 	g_lingo->printArgs("MMovieXObj::m_writeFile", nargs);
 	if (nargs != 3) {
 		warning("MMovieXObj::m_writeFile(): expecting 3 arguments");
+		g_lingo->dropStack(nargs);
+		g_lingo->push(Datum(""));
+		return;
 	}
 	Common::SaveFileManager *saves = g_system->getSavefileManager();
 	bool scramble = g_lingo->pop().asInt() != 0;
diff --git a/engines/director/sprite.cpp b/engines/director/sprite.cpp
index 351169fae13..73160fa54fc 100644
--- a/engines/director/sprite.cpp
+++ b/engines/director/sprite.cpp
@@ -458,13 +458,10 @@ void Sprite::setCast(CastMemberID memberID, bool replaceDims) {
 		if (replaceDims) {
 			Common::Rect dims = _cast->getInitialRect();
 			switch (_cast->_type) {
-			case kCastBitmap:
-				_width = dims.width();
-				_height = dims.height();
-				break;
 			case kCastShape:
 			case kCastText: 	// fall-through
 				break;
+			case kCastBitmap:
 			default:
 				_width = dims.width();
 				_height = dims.height();


Commit: 1c07d3e9aa15384a2a3a7bf21c37cbc40048390a
    https://github.com/scummvm/scummvm/commit/1c07d3e9aa15384a2a3a7bf21c37cbc40048390a
Author: Scott Percival (code at moral.net.au)
Date: 2024-05-12T22:00:19+02:00

Commit Message:
DIRECTOR: Move save modal glue into function

Changed paths:
    engines/director/lingo/xlibs/fileio.cpp
    engines/director/lingo/xlibs/mmovie.cpp
    engines/director/util.cpp
    engines/director/util.h


diff --git a/engines/director/lingo/xlibs/fileio.cpp b/engines/director/lingo/xlibs/fileio.cpp
index 44526d233cf..d7285449914 100644
--- a/engines/director/lingo/xlibs/fileio.cpp
+++ b/engines/director/lingo/xlibs/fileio.cpp
@@ -120,18 +120,16 @@ delete object me -- deletes the open file
 
  */
 
-#include "gui/filebrowser-dialog.h"
-
 #include "common/file.h"
 #include "common/memstream.h"
 #include "common/savefile.h"
 
 #include "director/director.h"
+#include "director/util.h"
 #include "director/lingo/lingo.h"
 #include "director/lingo/lingo-object.h"
 #include "director/lingo/lingo-utils.h"
 #include "director/lingo/xlibs/fileio.h"
-#include "savestate.h"
 
 namespace Director {
 
@@ -226,14 +224,11 @@ FileIOError FileObject::open(const Common::String &origpath, const Common::Strin
 
 	if (option.hasPrefix("?")) {
 		option = option.substr(1);
-		Common::String mask = prefix + "*.txt";
-		dirSeparator = '/';
-
-		GUI::FileBrowserDialog browser(nullptr, "txt", option.equalsIgnoreCase("write") ? GUI::kFBModeSave : GUI::kFBModeLoad, mask.c_str(), origpath.c_str());
-		if (browser.runModal() <= 0) {
+		path = getFileNameFromModal(option.equalsIgnoreCase("write"), origpath, "txt");
+		if (path.empty()) {
 			return kErrorFileNotFound;
 		}
-		path = browser.getResult();
+		dirSeparator = '/';
 	} else if (!path.hasSuffixIgnoreCase(".txt")) {
 		path += ".txt";
 	}
@@ -379,27 +374,11 @@ void FileIO::m_closeFile(int nargs) {
 XOBJSTUB(FileIO::m_createFile, 0);
 
 void FileIO::m_displayOpen(int nargs) {
-	Common::String prefix = g_director->getTargetName() + '-';
-	Common::String mask = prefix + "*.txt";
-
-	GUI::FileBrowserDialog browser(nullptr, "txt", GUI::kFBModeLoad, mask.c_str());
-	Datum result("");
-	if (browser.runModal() > 0) {
-		result = browser.getResult();
-	}
-	g_lingo->push(result);
+	g_lingo->push(getFileNameFromModal(false, Common::String(), "txt"));
 }
 
 void FileIO::m_displaySave(int nargs) {
-	Common::String prefix = g_director->getTargetName() + '-';
-	Common::String mask = prefix + "*.txt";
-
-	GUI::FileBrowserDialog browser(nullptr, "txt", GUI::kFBModeSave, mask.c_str());
-	Datum result("");
-	if (browser.runModal() > 0) {
-		result = browser.getResult();
-	}
-	g_lingo->push(result);
+	g_lingo->push(getFileNameFromModal(true, Common::String(), "txt"));
 }
 
 XOBJSTUB(FileIO::m_setFilterMask, 0)
diff --git a/engines/director/lingo/xlibs/mmovie.cpp b/engines/director/lingo/xlibs/mmovie.cpp
index 95645c72f0c..056b5aeaf51 100644
--- a/engines/director/lingo/xlibs/mmovie.cpp
+++ b/engines/director/lingo/xlibs/mmovie.cpp
@@ -558,16 +558,12 @@ void MMovieXObj::m_readFile(int nargs) {
 	Common::String prefix = g_director->getTargetName() + '-';
 	Common::String result;
 	if (origPath.empty()) {
-		Common::String mask = prefix + "*.txt";
-
-		GUI::FileBrowserDialog browser(nullptr, "txt", GUI::kFBModeLoad, mask.c_str());
-		if (browser.runModal() <= 0) {
+		path = getFileNameFromModal(false, Common::String(), "txt");
+		if (path.empty()) {
 			debugC(5, kDebugXObj, "MMovieXObj::m_readFile(): read cancelled by modal");
 			g_lingo->push(result);
 			return;
 		}
-		path = browser.getResult();
-
 	} else {
 		path = lastPathComponent(origPath, g_director->_dirSeparator);
 		if (path.hasSuffixIgnoreCase(".txt"))
@@ -623,16 +619,12 @@ void MMovieXObj::m_writeFile(int nargs) {
 
 	Common::String prefix = g_director->getTargetName() + '-';
 	if (origPath.empty()) {
-		Common::String mask = prefix + "*.txt";
-
-		GUI::FileBrowserDialog browser(nullptr, "txt", GUI::kFBModeSave, mask.c_str());
-		if (browser.runModal() <= 0) {
-			debugC(5, kDebugXObj, "MMovieXObj::m_writeFile(): write cancelled by modal");
+		path = getFileNameFromModal(false, Common::String(), "txt");
+		if (path.empty()) {
+			debugC(5, kDebugXObj, "MMovieXObj::m_writeFile(): read cancelled by modal");
 			g_lingo->push(result);
 			return;
 		}
-		path = browser.getResult();
-
 	} else {
 		path = lastPathComponent(origPath, g_director->_dirSeparator);
 		if (path.hasSuffixIgnoreCase(".txt"))
diff --git a/engines/director/util.cpp b/engines/director/util.cpp
index 7cd33acb2be..41897e2b649 100644
--- a/engines/director/util.cpp
+++ b/engines/director/util.cpp
@@ -30,6 +30,7 @@
 
 #include "director/types.h"
 #include "graphics/macgui/macwindowmanager.h"
+#include "gui/filebrowser-dialog.h"
 
 #include "director/director.h"
 #include "director/movie.h"
@@ -972,6 +973,24 @@ Common::Path findAudioPath(const Common::String &path, bool currentFolder, bool
 	return result;
 }
 
+Common::String getFileNameFromModal(bool save, const Common::String &suggested, const char *ext) {
+	Common::String prefix = g_director->getTargetName() + '-';
+	Common::String mask = prefix + "*";
+	if (ext) {
+		mask += ".";
+		mask += ext;
+	}
+	GUI::FileBrowserDialog browser(nullptr, "txt", save ? GUI::kFBModeSave : GUI::kFBModeLoad, mask.c_str(), suggested.c_str());
+	if (browser.runModal() <= 0) {
+		return Common::String();
+	}
+	Common::String result = browser.getResult();
+	if (!result.empty() && !result.hasPrefixIgnoreCase(prefix))
+		result = prefix + result;
+	return result;
+}
+
+
 bool hasExtension(Common::String filename) {
 	uint len = filename.size();
 	return len >= 4 && filename[len - 4] == '.'
diff --git a/engines/director/util.h b/engines/director/util.h
index 0364cb0fc7e..73815b6b7ac 100644
--- a/engines/director/util.h
+++ b/engines/director/util.h
@@ -54,6 +54,7 @@ Common::Path findMoviePath(const Common::String &path, bool currentFolder = true
 Common::Path findXLibPath(const Common::String &path, bool currentFolder = true, bool searchPaths = true);
 Common::Path findAudioPath(const Common::String &path, bool currentFolder = true, bool searchPaths = true);
 
+Common::String getFileNameFromModal(bool save, const Common::String &suggested, const char *ext = "txt");
 
 bool hasExtension(Common::String filename);
 


Commit: a1d692963dbfe0c2dd1d105ec641f4dbdb5e5167
    https://github.com/scummvm/scummvm/commit/a1d692963dbfe0c2dd1d105ec641f4dbdb5e5167
Author: Scott Percival (code at moral.net.au)
Date: 2024-05-12T22:00:19+02:00

Commit Message:
DIRECTOR: LINGO: Track ScriptContext properties in addition order

Changed paths:
    engines/director/castmember/castmember.cpp
    engines/director/castmember/castmember.h
    engines/director/debugtools.cpp
    engines/director/lingo/lingo-builtins.cpp
    engines/director/lingo/lingo-bytecode.cpp
    engines/director/lingo/lingo-codegen.cpp
    engines/director/lingo/lingo-object.cpp
    engines/director/lingo/lingo-object.h
    engines/director/lingo/lingo.cpp
    engines/director/window.h


diff --git a/engines/director/castmember/castmember.cpp b/engines/director/castmember/castmember.cpp
index 93bfb301f1e..9193dc78122 100644
--- a/engines/director/castmember/castmember.cpp
+++ b/engines/director/castmember/castmember.cpp
@@ -100,7 +100,7 @@ Datum CastMember::getProp(const Common::String &propName) {
 	return Datum();
 }
 
-bool CastMember::setProp(const Common::String &propName, const Datum &value) {
+bool CastMember::setProp(const Common::String &propName, const Datum &value, bool force) {
 	Common::String fieldName = Common::String::format("%d%s", kTheCast, propName.c_str());
 	if (g_lingo->_theEntityFields.contains(fieldName)) {
 		return setField(g_lingo->_theEntityFields[fieldName]->field, value);
diff --git a/engines/director/castmember/castmember.h b/engines/director/castmember/castmember.h
index 4e200aaa4dc..c7a945a52c9 100644
--- a/engines/director/castmember/castmember.h
+++ b/engines/director/castmember/castmember.h
@@ -73,7 +73,7 @@ public:
 
 	bool hasProp(const Common::String &propName) override;
 	Datum getProp(const Common::String &propName) override;
-	bool setProp(const Common::String &propName, const Datum &value) override;
+	bool setProp(const Common::String &propName, const Datum &value, bool force = false) override;
 	bool hasField(int field) override;
 	Datum getField(int field) override;
 	bool setField(int field, const Datum &value) override;
diff --git a/engines/director/debugtools.cpp b/engines/director/debugtools.cpp
index 58633332b4f..5d64c7806c8 100644
--- a/engines/director/debugtools.cpp
+++ b/engines/director/debugtools.cpp
@@ -679,12 +679,12 @@ static void showVars() {
 		if (lingo->_state->me.type == OBJECT && lingo->_state->me.u.obj->getObjType() & (kFactoryObj | kScriptObj)) {
 			ScriptContext *script = static_cast<ScriptContext *>(lingo->_state->me.u.obj);
 			ImGui::TextColored(head_color, "Instance/property vars:");
-			for (auto &it : script->_properties) {
-				keyBuffer.push_back(it._key);
+			for (uint32 i = 1; i <= script->getPropCount(); i++) {
+				keyBuffer.push_back(script->getPropAt(i));
 			}
 			Common::sort(keyBuffer.begin(), keyBuffer.end());
 			for (auto &i : keyBuffer) {
-				Datum &val = script->_properties.getVal(i);
+				Datum val = script->getProp(i);
 				displayVariable(i);
 				ImGui::SameLine();
 				ImGui::Text(" - [%s] %s", val.type2str(), formatStringForDump(val.asString(true)).c_str());
diff --git a/engines/director/lingo/lingo-builtins.cpp b/engines/director/lingo/lingo-builtins.cpp
index d5be3cd8824..cffd1aa1c9c 100644
--- a/engines/director/lingo/lingo-builtins.cpp
+++ b/engines/director/lingo/lingo-builtins.cpp
@@ -453,10 +453,10 @@ void LB::b_random(int nargs) {
 	Datum res;
 	// Output in D4/D5 seems to be bounded from 1-65535, regardless of input.
 	if (max <= 0) {
-		res = g_director->_rnd.getRandom(65535) + 1;
+		res = Datum(g_director->_rnd.getRandom(65535) + 1);
 	} else {
 		max = MIN(max, 65535);
-		res = g_director->_rnd.getRandom(max) + 1;
+		res = Datum(g_director->_rnd.getRandom(max) + 1);
 	}
 	g_lingo->push(res);
 }
@@ -962,7 +962,7 @@ void LB::b_getProp(int nargs) {
 	case OBJECT:
 		{
 			if (prop.type != SYMBOL) {
-				g_lingo->lingoError("b_getProp(): symbol expected");
+				g_lingo->lingoError("BUILDBOT: b_getProp(): symbol expected, got %s", prop.type2str());
 				return;
 			}
 			Datum d;
@@ -1184,7 +1184,7 @@ void LB::b_setProp(int nargs) {
 	case OBJECT:
 		{
 			if (prop.type != SYMBOL) {
-				g_lingo->lingoError("b_setProp(): symbol expected");
+				g_lingo->lingoError("BUILDBOT: b_setProp(): symbol expected, got %s", prop.type2str());
 				return;
 			}
 			// unlike the PARRAY case, OBJECT seems to create
diff --git a/engines/director/lingo/lingo-bytecode.cpp b/engines/director/lingo/lingo-bytecode.cpp
index 00da9e92575..b341e87f3f0 100644
--- a/engines/director/lingo/lingo-bytecode.cpp
+++ b/engines/director/lingo/lingo-bytecode.cpp
@@ -1115,7 +1115,7 @@ ScriptContext *LingoCompiler::compileLingoV4(Common::SeekableReadStreamEndian &s
 		} else if (0 <= index && index < (int16)archive->names.size()) {
 			const char *name = archive->names[index].c_str();
 			debugC(5, kDebugLoading, "%d: %s", i, name);
-			_assemblyContext->_properties[name] = Datum();
+			_assemblyContext->setProp(name, Datum(), true);
 		} else {
 			warning("Property %d has unknown name id %d, skipping define", i, index);
 		}
diff --git a/engines/director/lingo/lingo-codegen.cpp b/engines/director/lingo/lingo-codegen.cpp
index 7b76a506877..9e826671a12 100644
--- a/engines/director/lingo/lingo-codegen.cpp
+++ b/engines/director/lingo/lingo-codegen.cpp
@@ -389,8 +389,8 @@ void LingoCompiler::registerMethodVar(const Common::String &name, VarType type)
 		}
 		(*_methodVars)[name] = type;
 		if (type == kVarProperty || type == kVarInstance) {
-			if (!_assemblyContext->_properties.contains(name))
-				_assemblyContext->_properties[name] = Datum();
+			if (!_assemblyContext->hasProp(name))
+				_assemblyContext->setProp(name, Datum(), true);
 		} else if (type == kVarGlobal) {
 			if (!g_lingo->_globalvars.contains(name))
 				g_lingo->_globalvars[name] = Datum();
@@ -473,8 +473,8 @@ bool LingoCompiler::visitHandlerNode(HandlerNode *node) {
 		if (i._value == kVarGlobal)
 			registerMethodVar(i._key, kVarGlobal);
 	}
-	for (auto &i : _assemblyContext->_properties) {
-		registerMethodVar(i._key, _inFactory ? kVarInstance : kVarProperty);
+	for (uint32 i = 1; i <= _assemblyContext->getPropCount(); i++) {
+		registerMethodVar(_assemblyContext->getPropAt(i), _inFactory ? kVarInstance : kVarProperty);
 	}
 
 	COMPILE_LIST(node->stmts);
diff --git a/engines/director/lingo/lingo-object.cpp b/engines/director/lingo/lingo-object.cpp
index 4efe7ce0715..3a5655df88f 100644
--- a/engines/director/lingo/lingo-object.cpp
+++ b/engines/director/lingo/lingo-object.cpp
@@ -416,6 +416,7 @@ ScriptContext::ScriptContext(const ScriptContext &sc) : Object<ScriptContext>(sc
 	}
 	_constants = sc._constants;
 	_properties = sc._properties;
+	_propertyNames = sc._propertyNames;
 
 	_id = sc._id;
 }
@@ -505,15 +506,15 @@ Datum ScriptContext::getProp(const Common::String &propName) {
 			return _properties["ancestor"].u.obj->getProp(propName);
 		}
 	}
+	_propertyNames.push_back(propName);
 	return _properties[propName]; // return new property
 }
 
 Common::String ScriptContext::getPropAt(uint32 index) {
-	// FIXME: Refactor the whole thing to have ordered keys
 	uint32 target = 1;
-	for (auto &it : _properties) {
+	for (auto &it : _propertyNames) {
 		if (target == index) {
-			return it._key;
+			return it;
 		}
 		target += 1;
 	}
@@ -521,10 +522,10 @@ Common::String ScriptContext::getPropAt(uint32 index) {
 }
 
 uint32 ScriptContext::getPropCount() {
-	return _properties.size();
+	return _propertyNames.size();
 }
 
-bool ScriptContext::setProp(const Common::String &propName, const Datum &value) {
+bool ScriptContext::setProp(const Common::String &propName, const Datum &value, bool force) {
 	if (_disposed) {
 		error("Property '%s' accessed on disposed object <%s>", propName.c_str(), Datum(this).asString(true).c_str());
 	}
@@ -532,14 +533,20 @@ bool ScriptContext::setProp(const Common::String &propName, const Datum &value)
 		_properties[propName] = value;
 		return true;
 	}
-	if (_objType == kScriptObj) {
+	if (force) {
+		// used by e.g. the script compiler to add properties
+		_propertyNames.push_back(propName);
+		_properties[propName] = value;
+		return true;
+	} else if (_objType == kScriptObj) {
 		if (_properties.contains("ancestor") && _properties["ancestor"].type == OBJECT
 				&& (_properties["ancestor"].u.obj->getObjType() & (kScriptObj | kXtraObj))) {
 			debugC(3, kDebugLingoExec, "Getting prop '%s' from ancestor: <%s>", propName.c_str(), _properties["ancestor"].asString(true).c_str());
-			return _properties["ancestor"].u.obj->setProp(propName, value);
+			return _properties["ancestor"].u.obj->setProp(propName, value, force);
 		}
 	} else if (_objType == kFactoryObj) {
 		// D3 style anonymous objects/factories, set whatever properties you like
+		_propertyNames.push_back(propName);
 		_properties[propName] = value;
 		return true;
 	}
@@ -659,7 +666,7 @@ Datum Window::getProp(const Common::String &propName) {
 	return Datum();
 }
 
-bool Window::setProp(const Common::String &propName, const Datum &value) {
+bool Window::setProp(const Common::String &propName, const Datum &value, bool force) {
 	Common::String fieldName = Common::String::format("%d%s", kTheWindow, propName.c_str());
 	if (g_lingo->_theEntityFields.contains(fieldName)) {
 		return setField(g_lingo->_theEntityFields[fieldName]->field, value);
diff --git a/engines/director/lingo/lingo-object.h b/engines/director/lingo/lingo-object.h
index 09446e6dc25..53f60a1da0d 100644
--- a/engines/director/lingo/lingo-object.h
+++ b/engines/director/lingo/lingo-object.h
@@ -56,7 +56,7 @@ public:
 	virtual Datum getProp(const Common::String &propName) = 0;
 	virtual Common::String getPropAt(uint32 index) = 0;
 	virtual uint32 getPropCount() = 0;
-	virtual bool setProp(const Common::String &propName, const Datum &value) = 0;
+	virtual bool setProp(const Common::String &propName, const Datum &value, bool force = false) = 0;
 	virtual bool hasField(int field) = 0;
 	virtual Datum getField(int field) = 0;
 	virtual bool setField(int field, const Datum &value) = 0;
@@ -181,7 +181,7 @@ public:
 	uint32 getPropCount() override {
 		return 0;
 	};
-	bool setProp(const Common::String &propName, const Datum &value) override {
+	bool setProp(const Common::String &propName, const Datum &value, bool force = false) override {
 		return false;
 	};
 	bool hasField(int field) override {
@@ -213,11 +213,12 @@ public:
 	SymbolHash _functionHandlers;
 	Common::HashMap<uint32, Symbol> _eventHandlers;
 	Common::Array<Datum> _constants;
-	DatumHash _properties;
 	Common::HashMap<uint32, Datum> _objArray;
 	MethodHash _methodNames;
 
 private:
+	DatumHash _properties;
+	Common::Array<Common::String> _propertyNames;
 	bool _onlyInLctxContexts = false;
 
 public:
@@ -237,7 +238,7 @@ public:
 	Datum getProp(const Common::String &propName) override;
 	Common::String getPropAt(uint32 index) override;
 	uint32 getPropCount() override;
-	bool setProp(const Common::String &propName, const Datum &value) override;
+	bool setProp(const Common::String &propName, const Datum &value, bool force = false) override;
 
 	Symbol define(const Common::String &name, ScriptData *code, Common::Array<Common::String> *argNames, Common::Array<Common::String> *varNames);
 
diff --git a/engines/director/lingo/lingo.cpp b/engines/director/lingo/lingo.cpp
index e8b501ad5bb..dc1ec563c00 100644
--- a/engines/director/lingo/lingo.cpp
+++ b/engines/director/lingo/lingo.cpp
@@ -108,7 +108,7 @@ Symbol& Symbol::operator=(const Symbol &s) {
 }
 
 bool Symbol::operator==(Symbol &s) const {
-	return ctx == s.ctx && (*name == *s.name);
+	return ctx == s.ctx && (name->equalsIgnoreCase(*s.name));
 }
 
 void Symbol::reset() {
@@ -1556,12 +1556,12 @@ Common::String Lingo::formatAllVars() {
 	if (_state->me.type == OBJECT && _state->me.u.obj->getObjType() & (kFactoryObj | kScriptObj)) {
 		ScriptContext *script = static_cast<ScriptContext *>(_state->me.u.obj);
 		result += Common::String("  Instance/property vars: \n");
-		for (auto &it : script->_properties) {
-			keyBuffer.push_back(it._key);
+		for (uint32 i = 1; i <= script->getPropCount(); i++) {
+			keyBuffer.push_back(script->getPropAt(i));
 		}
 		Common::sort(keyBuffer.begin(), keyBuffer.end());
 		for (auto &i : keyBuffer) {
-			Datum &val = script->_properties.getVal(i);
+			Datum val = script->getProp(i);
 			result += Common::String::format("    %s - [%s] %s\n", i.c_str(), val.type2str(), formatStringForDump(val.asString(true)).c_str());
 		}
 		keyBuffer.clear();
diff --git a/engines/director/window.h b/engines/director/window.h
index 8e5f609f4ff..451a17424e2 100644
--- a/engines/director/window.h
+++ b/engines/director/window.h
@@ -185,7 +185,7 @@ public:
 	Common::String asString() override;
 	bool hasProp(const Common::String &propName) override;
 	Datum getProp(const Common::String &propName) override;
-	bool setProp(const Common::String &propName, const Datum &value) override;
+	bool setProp(const Common::String &propName, const Datum &value, bool force = false) override;
 	bool hasField(int field) override;
 	Datum getField(int field) override;
 	bool setField(int field, const Datum &value) override;


Commit: 13fa4baf7288785f812246083768d592ff3523ba
    https://github.com/scummvm/scummvm/commit/13fa4baf7288785f812246083768d592ff3523ba
Author: Scott Percival (code at moral.net.au)
Date: 2024-05-12T22:00:19+02:00

Commit Message:
DIRECTOR: Split savename prefix into common method

Changed paths:
    engines/director/game-quirks.cpp
    engines/director/lingo/xlibs/dialogsxobj.cpp
    engines/director/lingo/xlibs/fileio.cpp
    engines/director/lingo/xlibs/mmovie.cpp
    engines/director/lingo/xlibs/valkyrie.cpp
    engines/director/util.cpp
    engines/director/util.h


diff --git a/engines/director/game-quirks.cpp b/engines/director/game-quirks.cpp
index 219085ed08a..78341825bfe 100644
--- a/engines/director/game-quirks.cpp
+++ b/engines/director/game-quirks.cpp
@@ -268,7 +268,7 @@ void DirectorEngine::gameQuirks(const char *target, Common::Platform platform) {
 				// Inject files from the save game storage into the path
 				Common::SaveFileManager *saves = g_system->getSavefileManager();
 				// As save games are name-mangled by FileIO, demangle them here
-				Common::String prefix = g_director->getTargetName() + '-' + '*';
+				Common::String prefix = savePrefix() + '*';
 				for (auto &it : saves->listSavefiles(prefix.c_str())) {
 					Common::String demangled = f->path + it.substr(prefix.size() - 1);
 					if (demangled.hasSuffixIgnoreCase(".txt")) {
diff --git a/engines/director/lingo/xlibs/dialogsxobj.cpp b/engines/director/lingo/xlibs/dialogsxobj.cpp
index ef3be9dfab6..13140aaf278 100644
--- a/engines/director/lingo/xlibs/dialogsxobj.cpp
+++ b/engines/director/lingo/xlibs/dialogsxobj.cpp
@@ -102,7 +102,7 @@ void DialogsXObj::m_putFile(int nargs) {
 	Common::String name = g_lingo->pop().asString();
 	Common::String title = g_lingo->pop().asString();
 
-	Common::String prefix = g_director->getTargetName() + '-';
+	Common::String prefix = savePrefix();
 	Common::String mask = prefix + "*." + extn + ".txt";
 	Common::String filename = name;
 
@@ -126,7 +126,7 @@ void DialogsXObj::m_getFile(int nargs) {
 	Common::String name = g_lingo->pop().asString();
 	Common::String title = g_lingo->pop().asString();
 
-	Common::String prefix = g_director->getTargetName() + '-';
+	Common::String prefix = savePrefix();
 	Common::String mask = prefix + "*." + extn + ".txt";
 	Common::String fileName = name;
 
diff --git a/engines/director/lingo/xlibs/fileio.cpp b/engines/director/lingo/xlibs/fileio.cpp
index d7285449914..c8c25600dc2 100644
--- a/engines/director/lingo/xlibs/fileio.cpp
+++ b/engines/director/lingo/xlibs/fileio.cpp
@@ -220,7 +220,7 @@ FileIOError FileObject::open(const Common::String &origpath, const Common::Strin
 	Common::String option = mode;
 	char dirSeparator = g_director->_dirSeparator;
 
-	Common::String prefix = g_director->getTargetName() + '-';
+	Common::String prefix = savePrefix();
 
 	if (option.hasPrefix("?")) {
 		option = option.substr(1);
@@ -576,7 +576,7 @@ void FileIO::m_fileName(int nargs) {
 	FileObject *me = static_cast<FileObject *>(g_lingo->_state->me.u.obj);
 
 	if (me->_filename) {
-		Common::String prefix = g_director->getTargetName() + '-';
+		Common::String prefix = savePrefix();
 		Common::String res = *me->_filename;
 		if (res.hasPrefix(prefix)) {
 			res = Common::String(&me->_filename->c_str()[prefix.size()]);
diff --git a/engines/director/lingo/xlibs/mmovie.cpp b/engines/director/lingo/xlibs/mmovie.cpp
index 056b5aeaf51..01e445999c8 100644
--- a/engines/director/lingo/xlibs/mmovie.cpp
+++ b/engines/director/lingo/xlibs/mmovie.cpp
@@ -555,7 +555,7 @@ void MMovieXObj::m_readFile(int nargs) {
 	Common::String origPath = g_lingo->pop().asString();
 	Common::String path = origPath;
 
-	Common::String prefix = g_director->getTargetName() + '-';
+	Common::String prefix = savePrefix();
 	Common::String result;
 	if (origPath.empty()) {
 		path = getFileNameFromModal(false, Common::String(), "txt");
@@ -617,7 +617,7 @@ void MMovieXObj::m_writeFile(int nargs) {
 	Common::String path = origPath;
 	Common::String result;
 
-	Common::String prefix = g_director->getTargetName() + '-';
+	Common::String prefix = savePrefix();
 	if (origPath.empty()) {
 		path = getFileNameFromModal(false, Common::String(), "txt");
 		if (path.empty()) {
diff --git a/engines/director/lingo/xlibs/valkyrie.cpp b/engines/director/lingo/xlibs/valkyrie.cpp
index d73f91f687f..cf2a8ba1fd0 100644
--- a/engines/director/lingo/xlibs/valkyrie.cpp
+++ b/engines/director/lingo/xlibs/valkyrie.cpp
@@ -108,7 +108,7 @@ XOBJSTUB(ValkyrieXObj::m_lastError, "")
 void ValkyrieXObj::m_save(int nargs) {
 	// should write to namco.ini > Valkyrie > Data
 	// TODO: Should report errors if we fail to save
-	Common::String saveName = g_director->getTargetName() + "-namco.ini.txt";
+	Common::String saveName = savePrefix() + "namco.ini.txt";
 	Common::String saveString = g_lingo->pop().asString();
 	Common::INIFile *saveFile = new Common::INIFile();
 	saveFile->loadFromSaveFile(saveName);
@@ -123,7 +123,7 @@ void ValkyrieXObj::m_load(int nargs) {
 	// TODO: Report errors if we fail to load?
 	Common::String saveString;
 	Common::INIFile *saveFile = new Common::INIFile();
-	saveFile->loadFromSaveFile(g_director->getTargetName() + "-namco.ini.txt");
+	saveFile->loadFromSaveFile(savePrefix() + "namco.ini.txt");
 	if (!(saveFile->hasKey("Data", "Valkyrie"))) {
 		saveString = "0NAX";
 	} else {
diff --git a/engines/director/util.cpp b/engines/director/util.cpp
index 41897e2b649..cab1244f49e 100644
--- a/engines/director/util.cpp
+++ b/engines/director/util.cpp
@@ -974,7 +974,7 @@ Common::Path findAudioPath(const Common::String &path, bool currentFolder, bool
 }
 
 Common::String getFileNameFromModal(bool save, const Common::String &suggested, const char *ext) {
-	Common::String prefix = g_director->getTargetName() + '-';
+	Common::String prefix = savePrefix();
 	Common::String mask = prefix + "*";
 	if (ext) {
 		mask += ".";
@@ -990,6 +990,10 @@ Common::String getFileNameFromModal(bool save, const Common::String &suggested,
 	return result;
 }
 
+Common::String savePrefix() {
+	return g_director->getTargetName() + '-';
+}
+
 
 bool hasExtension(Common::String filename) {
 	uint len = filename.size();
diff --git a/engines/director/util.h b/engines/director/util.h
index 73815b6b7ac..a9cb0d2a6bb 100644
--- a/engines/director/util.h
+++ b/engines/director/util.h
@@ -55,6 +55,7 @@ Common::Path findXLibPath(const Common::String &path, bool currentFolder = true,
 Common::Path findAudioPath(const Common::String &path, bool currentFolder = true, bool searchPaths = true);
 
 Common::String getFileNameFromModal(bool save, const Common::String &suggested, const char *ext = "txt");
+Common::String savePrefix();
 
 bool hasExtension(Common::String filename);
 




More information about the Scummvm-git-logs mailing list