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

sev- noreply at scummvm.org
Tue Jan 30 22:30:19 UTC 2024


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

Summary:
03aa341d0b DIRECTOR: Add patcher for individual script handlers
294f8608a1 DIRECTOR: Fix the commandKey behaviour for Windows
b23b084232 DIRECTOR: Add property breakpoints to debugger
b1e4e92ed2 DIRECTOR: Remove channel copying bodge
5e18b53e93 DIRECTOR: Fix use-after-free in EXE loader
bfb10b7187 DIRECTOR: Fix bounds of kTransPushUp
2d1094a091 DIRECTOR: LINGO: Allow reversed bound args to b_spriteBox
27f0e8f8bb DIRECTOR: Rework event ordering to be closer to original
b50ed6ace4 DIRECTOR: Don't render transitions when movie is paused
5d69024aa4 DIRECTOR: Clean up Score methods that previously needed frameId
c4fe1add55 DIRECTOR: Persist editable status on sprites if cast is the same


Commit: 03aa341d0bcc64249ce9ec4cf849cd4b8fc42f8a
    https://github.com/scummvm/scummvm/commit/03aa341d0bcc64249ce9ec4cf849cd4b8fc42f8a
Author: Scott Percival (code at moral.net.au)
Date: 2024-01-30T23:30:11+01:00

Commit Message:
DIRECTOR: Add patcher for individual script handlers

This gives the ability to replace function handlers in a compiled Lingo
script with a replacement from the quirks table. The replacement can be
written in Lingo; no need to worry about injecting bytecode.

As an example, here's a replacement for Cosmology of Kyoto's text input
handler. The original was notorious for requiring an exact match against a
word list containing typos, along with extra spaces and punctuation. The
new one is more forgiving, and always supports the answers "yes" and
"no".

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


diff --git a/engines/director/cast.cpp b/engines/director/cast.cpp
index 3e3cd664389..9a4bc799312 100644
--- a/engines/director/cast.cpp
+++ b/engines/director/cast.cpp
@@ -48,6 +48,7 @@
 #include "director/castmember/sound.h"
 #include "director/castmember/text.h"
 #include "director/castmember/transition.h"
+#include "director/lingo/lingo-codegen.h"
 
 namespace Director {
 
@@ -1088,6 +1089,7 @@ void Cast::loadLingoContext(Common::SeekableReadStreamEndian &stream) {
 					error("Cast::loadLingoContext: Script already defined for type %s, id %d", scriptType2str(script->_scriptType), script->_id);
 				}
 				_lingoArchive->scriptContexts[script->_scriptType][script->_id] = script;
+				_lingoArchive->patchScriptHandler(script->_scriptType, CastMemberID(script->_id, _castLibID));
 			} else {
 				// Keep track of scripts that are not in scriptContexts
 				// Those scripts need to be cleaned up on ~LingoArchive
diff --git a/engines/director/lingo/lingo-patcher.cpp b/engines/director/lingo/lingo-patcher.cpp
index 2e31b8829a0..18cce8e4b55 100644
--- a/engines/director/lingo/lingo-patcher.cpp
+++ b/engines/director/lingo/lingo-patcher.cpp
@@ -247,6 +247,128 @@ struct ScriptPatch {
 	{nullptr, nullptr, kPlatformUnknown, nullptr, kNoneScript, 0, 0, 0, nullptr, nullptr}
 };
 
+/*
+ * Cosmology of Kyoto has a text entry system, however for the English version
+ * at least you are very unlikely to guess the correct sequence of letters that
+ * constitute a valid answer. This is an attempt to make things fairer by removing
+ * the need for precise whitespace and punctuation. As a fallback, "yes" should
+ * always mean a yes response, and "no" should always mean a no response.
+ */
+
+const char *kyotoTextEntryFix = " \
+on scrubInput inputString \r\
+  set result = \"\" \r\
+  repeat with x = 1 to the number of chars in inputString \r\
+    if chars(inputString, x, x) = \" \" then continue \r\
+	else if chars(inputString, x, x) = \".\" then continue \r\
+	else if chars(inputString, x, x) = \"!\" then continue \r\
+	else if chars(inputString, x, x) = \"?\" then continue \r\
+	else if chars(inputString, x, x) = \"。\" then continue \r\
+	else \r\
+      set result = result & char x of inputString \r\
+	end if \r\
+  end repeat \r\
+  return result \r\
+end \r\
+\r\
+on checkkaiwa kaiwatrue, kaiwafalse \r\
+  global myparadata \r\
+  if (keyCode() <> 36) and (keyCode() <> 76) then \r\
+    exit \r\
+  end if \r\
+  put \"Original YES options: \" & kaiwatrue \r\
+  put \"Original NO options: \" & kaiwafalse \r\
+  -- pre-scrub all input and choices to remove effect of whitespace/punctuation \r\
+  set kaiwaans = scrubInput(field \"KaiwaWindow\") \r\
+  set kaiwatrue = scrubInput(kaiwatrue) \r\
+  set kaiwafalse = scrubInput(kaiwafalse) \r\
+  -- yes and no should always give consistent results \r\
+  if kaiwaans = \"yes\" then \r\
+    return \"YES\" \r\
+  else if kaiwaans = \"no\" then \r\
+    return \"NO\" \r\
+  end if \r\
+  repeat with y = 1 to the number of items in kaiwatrue \r\
+    if item y of kaiwatrue starts kaiwaans then \r\
+      when keyDown then CheckQuit \r\
+      put EMPTY into field \"KaiwaWindow\" \r\
+      return \"YES\" \r\
+    end if \r\
+  end repeat \r\
+  repeat with n = 1 to the number of items in kaiwafalse \r\
+    if item n of kaiwafalse starts kaiwaans then \r\
+      when keyDown then CheckQuit \r\
+      put EMPTY into field \"KaiwaWindow\" \r\
+      return \"NO\" \r\
+    end if \r\
+  end repeat \r\
+    set kaiwafool to scrubInput(\"あほんだら,ばか,うんこ,しっこ,しね,死ね,うるさい,うるせえ,\" & \"fool,simpleton,stupid person,kill,Shut up!,Get out of my hair!\") \r\
+  repeat with f = 1 to the number of items in kaiwafool \r\
+    if item f of kaiwafool starts kaiwaans then \r\
+      myparadata(maddparadata, 2, 1) \r\
+      when keyDown then CheckQuit \r\
+      put EMPTY into field \"KaiwaWindow\" \r\
+      return \"error\" \r\
+    end if \r\
+  end repeat \r\
+  when keyDown then CheckQuit \r\
+  put EMPTY into field \"KaiwaWindow\" \r\
+  return \"error\" \r\
+end \r\
+";
+
+struct ScriptHandlerPatch {
+	const char *gameId;
+	const char *extra;
+	Common::Platform platform; // Specify kPlatformUnknown for skipping platform check
+	const char *movie;
+	ScriptType type;
+	uint16 id;
+	uint16 castLib;
+	const char *handlerBody;
+} const scriptHandlerPatches[] = {
+	{"kyoto", nullptr, kPlatformWindows, "ck_data\\dd_dairi\\shared.dxr", kMovieScript, 906, DEFAULT_CAST_LIB, kyotoTextEntryFix},
+	{"kyoto", nullptr, kPlatformWindows, "ck_data\\findfldr\\shared.dxr", kMovieScript, 802, DEFAULT_CAST_LIB, kyotoTextEntryFix},
+	{"kyoto", nullptr, kPlatformWindows, "ck_data\\ichi\\shared.dxr", kMovieScript, 906, DEFAULT_CAST_LIB, kyotoTextEntryFix},
+	{"kyoto", nullptr, kPlatformWindows, "ck_data\\jigoku\\shared.dxr", kMovieScript, 840, DEFAULT_CAST_LIB, kyotoTextEntryFix},
+	{"kyoto", nullptr, kPlatformWindows, "ck_data\\kusamura\\shared.dxr", kMovieScript, 906, DEFAULT_CAST_LIB, kyotoTextEntryFix},
+	{"kyoto", nullptr, kPlatformWindows, "ck_data\\map01\\shared.dxr", kMovieScript, 906, DEFAULT_CAST_LIB, kyotoTextEntryFix},
+	{"kyoto", nullptr, kPlatformWindows, "ck_data\\map02\\shared.dxr", kMovieScript, 906, DEFAULT_CAST_LIB, kyotoTextEntryFix},
+	{"kyoto", nullptr, kPlatformWindows, "ck_data\\map03\\shared.dxr", kMovieScript, 906, DEFAULT_CAST_LIB, kyotoTextEntryFix},
+	{"kyoto", nullptr, kPlatformWindows, "ck_data\\map04\\shared.dxr", kMovieScript, 906, DEFAULT_CAST_LIB, kyotoTextEntryFix},
+	{"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},
+	{nullptr, nullptr, kPlatformUnknown, nullptr, kNoneScript, 0, 0, nullptr},
+
+};
+
+void LingoArchive::patchScriptHandler(ScriptType type, CastMemberID id) {
+	const ScriptHandlerPatch *patch = scriptHandlerPatches;
+	Common::String movie = g_director->getCurrentPath() + cast->getMacName();
+
+	// So far, we have not many patches, so do linear lookup
+	while (patch->gameId) {
+		// First, we do cheap comparisons
+		if (patch->type != type || patch->id != id.member || patch->castLib != id.castLib ||
+				(patch->platform != kPlatformUnknown && patch->platform != g_director->getPlatform())) {
+			patch++;
+			continue;
+		}
+
+		// Now expensive ones
+		U32String moviename = punycode_decode(patch->movie);
+		if (movie.compareToIgnoreCase(moviename) || strcmp(patch->gameId, g_director->getGameId())
+				|| (patch->extra && strcmp(patch->extra, g_director->getExtra()))) {
+			patch++;
+			continue;
+		}
+		patchCode(Common::U32String(patch->handlerBody), patch->type, patch->id);
+		patch++;
+	}
+}
+
+
 Common::U32String LingoCompiler::patchLingoCode(const Common::U32String &line, LingoArchive *archive, ScriptType type, CastMemberID id, int linenum) {
 	if (!archive)
 		return line;
diff --git a/engines/director/lingo/lingo.cpp b/engines/director/lingo/lingo.cpp
index fdfa90ea5aa..00c53a7eeee 100644
--- a/engines/director/lingo/lingo.cpp
+++ b/engines/director/lingo/lingo.cpp
@@ -338,6 +338,30 @@ Symbol Lingo::getHandler(const Common::String &name) {
 	return sym;
 }
 
+
+void LingoArchive::patchCode(const Common::U32String &code, ScriptType type, uint16 id, const char *scriptName, uint32 preprocFlags) {
+	debugC(1, kDebugCompile, "Patching code for type %s(%d) with id %d in '%s%s'\n"
+			"***********\n%s\n\n***********", scriptType2str(type), type, id, utf8ToPrintable(g_director->getCurrentPath()).c_str(), utf8ToPrintable(cast->getMacName()).c_str(), formatStringForDump(code.encode()).c_str());
+	if (!getScriptContext(type, id)) {
+		// If there's no existing script context, don't try and patch it.
+		warning("Script not defined for type %d, id %d", type, id);
+		return;
+	}
+
+	ScriptContext *sc = g_lingo->_compiler->compileLingo(code, nullptr, type, CastMemberID(id, cast->_castLibID), scriptName, false, preprocFlags);
+
+	if (sc) {
+		for (auto &it : sc->_functionHandlers) {
+			it._value.ctx = scriptContexts[type][id];
+			scriptContexts[type][id]->_functionHandlers[it._key] = it._value;
+			functionHandlers[it._key] = it._value;
+		}
+		sc->_functionHandlers.clear();
+		delete sc;
+	}
+}
+
+
 void LingoArchive::addCode(const Common::U32String &code, ScriptType type, uint16 id, const char *scriptName, uint32 preprocFlags) {
 	debugC(1, kDebugCompile, "Add code for type %s(%d) with id %d in '%s%s'\n"
 			"***********\n%s\n\n***********", scriptType2str(type), type, id, utf8ToPrintable(g_director->getCurrentPath()).c_str(), utf8ToPrintable(cast->getMacName()).c_str(), formatStringForDump(code.encode()).c_str());
diff --git a/engines/director/lingo/lingo.h b/engines/director/lingo/lingo.h
index 88ed62afae8..d7b7e6040ea 100644
--- a/engines/director/lingo/lingo.h
+++ b/engines/director/lingo/lingo.h
@@ -292,10 +292,14 @@ struct LingoArchive {
 	Common::String formatFunctionList(const char *prefix);
 
 	void addCode(const Common::U32String &code, ScriptType type, uint16 id, const char *scriptName = nullptr, uint32 preprocFlags = kLPPNone);
+	void patchCode(const Common::U32String &code, ScriptType type, uint16 id, const char *scriptName = nullptr, uint32 preprocFlags = kLPPNone);
 	void removeCode(ScriptType type, uint16 id);
 	void replaceCode(const Common::U32String &code, ScriptType type, uint16 id, const char *scriptName = nullptr);
 	void addCodeV4(Common::SeekableReadStreamEndian &stream, uint16 lctxIndex, const Common::String &archName, uint16 version);
 	void addNamesV4(Common::SeekableReadStreamEndian &stream);
+
+	// lingo-patcher.cpp
+	void patchScriptHandler(ScriptType type, CastMemberID id);
 };
 
 struct LingoState {


Commit: 294f8608a1fbb1f1d575d83e08631a621c48d208
    https://github.com/scummvm/scummvm/commit/294f8608a1fbb1f1d575d83e08631a621c48d208
Author: Scott Percival (code at moral.net.au)
Date: 2024-01-30T23:30:11+01:00

Commit Message:
DIRECTOR: Fix the commandKey behaviour for Windows

According to the Lingo manual, "the commandKey" checks the status of the
Ctrl key when running on Windows.

Fixes opening the save and exit screens in Cosmology of Kyoto for
Windows.

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 82c4ece96d4..3609c23249f 100644
--- a/engines/director/lingo/lingo-the.cpp
+++ b/engines/director/lingo/lingo-the.cpp
@@ -21,6 +21,7 @@
 
 #include "common/config-manager.h"
 #include "common/fs.h"
+#include "common/platform.h"
 #include "director/types.h"
 #include "graphics/macgui/macbutton.h"
 
@@ -429,7 +430,10 @@ Datum Lingo::getTheEntity(int entity, Datum &id, int field) {
 		d = 1;
 		break;
 	case kTheCommandDown:
-		d = (movie->_keyFlags & Common::KBD_META) ? 1 : 0;
+		if (g_director->getPlatform() == Common::kPlatformWindows)
+			d = (movie->_keyFlags & Common::KBD_CTRL) ? 1 : 0;
+		else
+			d = (movie->_keyFlags & Common::KBD_META) ? 1 : 0;
 		break;
 	case kTheControlDown:
 		d = (movie->_keyFlags & Common::KBD_CTRL) ? 1 : 0;


Commit: b23b0842325dcfa0ff12ba3d6dbe65d5898b3333
    https://github.com/scummvm/scummvm/commit/b23b0842325dcfa0ff12ba3d6dbe65d5898b3333
Author: Scott Percival (code at moral.net.au)
Date: 2024-01-30T23:30:11+01:00

Commit Message:
DIRECTOR: Add property breakpoints to debugger

Changed paths:
    engines/director/debugger.cpp
    engines/director/debugger.h
    engines/director/lingo/lingo-bytecode.cpp
    engines/director/lingo/lingo-the.cpp


diff --git a/engines/director/debugger.cpp b/engines/director/debugger.cpp
index bbe0f6b6b6a..68fddb88dde 100644
--- a/engines/director/debugger.cpp
+++ b/engines/director/debugger.cpp
@@ -94,6 +94,8 @@ Debugger::Debugger(): GUI::Debugger() {
 	registerCmd("bf", WRAP_METHOD(Debugger, cmdBpFrame));
 	registerCmd("bpentity", WRAP_METHOD(Debugger, cmdBpEntity));
 	registerCmd("be", WRAP_METHOD(Debugger, cmdBpEntity));
+	registerCmd("bpprop", WRAP_METHOD(Debugger, cmdBpProp));
+	registerCmd("bp", WRAP_METHOD(Debugger, cmdBpProp));
 	registerCmd("bpvar", WRAP_METHOD(Debugger, cmdBpVar));
 	registerCmd("bv", WRAP_METHOD(Debugger, cmdBpVar));
 	registerCmd("bpevent", WRAP_METHOD(Debugger, cmdBpEvent));
@@ -122,6 +124,8 @@ Debugger::Debugger(): GUI::Debugger() {
 	_bpCheckMoviePath = false;
 	_bpNextMovieMatch = false;
 	_bpMatchScriptId = 0;
+	_bpCheckPropRead = false;
+	_bpCheckPropWrite = false;
 	_bpCheckVarRead = false;
 	_bpCheckVarWrite = false;
 	_bpCheckEntityRead = false;
@@ -185,6 +189,8 @@ bool Debugger::cmdHelp(int argc, const char **argv) {
 	debugPrintf(" bpentity / be [entityName] [r/w/rw] - Create a breakpoint on a Lingo \"the\" entity being accessed in a specific way\n");
 	debugPrintf(" bpentity / be [entityName:fieldName] - Create a breakpoint on a Lingo \"the\" field being read or modified\n");
 	debugPrintf(" bpentity / be [entityName:fieldName] [r/w/rw] - Create a breakpoint on a Lingo \"the\" field being accessed in a specific way\n");
+	debugPrintf(" bpprop / bp [varName] - Create a breakpoint on a Lingo object property being read or modified\n");
+	debugPrintf(" bpprop / bp [varName] [r/w/rw] - Create a breakpoint on a Lingo object property being accessed in a specific way\n");
 	debugPrintf(" bpvar / bv [varName] - Create a breakpoint on a Lingo variable being read or modified\n");
 	debugPrintf(" bpvar / bv [varName] [r/w/rw] - Create a breakpoint on a Lingo variable being accessed in a specific way\n");
 	debugPrintf(" bpevent / bn [eventName] - Create a breakpoint on a Lingo event\n");
@@ -215,6 +221,11 @@ Common::String Breakpoint::format() {
 	case kBreakpointMovieFrame:
 		result += Common::String::format("Movie %s:%d", moviePath.c_str(), frameOffset);
 		break;
+	case kBreakpointProperty:
+		result += "Property "+ varName + ":";
+		result += varRead ? "r" : "";
+		result += varWrite ? "w" : "";
+		break;
 	case kBreakpointVariable:
 		result += "Variable "+ varName + ":";
 		result += varRead ? "r" : "";
@@ -763,6 +774,34 @@ bool Debugger::cmdBpEntity(int argc, const char **argv) {
 	return true;
 }
 
+bool Debugger::cmdBpProp(int argc, const char **argv) {
+	if (argc == 2 || argc == 3) {
+		Breakpoint bp;
+		bp.type = kBreakpointProperty;
+		bp.varName = argv[1];
+		if (argc == 3) {
+			Common::String props = argv[2];
+			bp.varRead = props.contains("r") || props.contains("R");
+			bp.varWrite = props.contains("w") || props.contains("W");
+			if (!(bp.varRead || bp.varWrite)) {
+				debugPrintf("Must specify r, w, or rw.");
+				return true;
+			}
+		} else {
+			bp.varRead = true;
+			bp.varWrite = true;
+		}
+		bp.id = _bpNextId;
+		_bpNextId++;
+		_breakpoints.push_back(bp);
+		bpUpdateState();
+		debugPrintf("Added %s\n", bp.format().c_str());
+	} else {
+		debugPrintf("Must specify a property.\n");
+	}
+	return true;
+}
+
 bool Debugger::cmdBpVar(int argc, const char **argv) {
 	if (argc == 2 || argc == 3) {
 		Breakpoint bp;
@@ -992,6 +1031,8 @@ void Debugger::bpUpdateState() {
 	_bpMatchScriptId = 0;
 	_bpMatchMoviePath.clear();
 	_bpMatchFrameOffsets.clear();
+	_bpCheckPropRead = false;
+	_bpCheckPropWrite = false;
 	_bpCheckVarRead = false;
 	_bpCheckVarWrite = false;
 	_bpCheckEntityRead = false;
@@ -1036,6 +1077,9 @@ void Debugger::bpUpdateState() {
 				_bpMatchMoviePath = it.moviePath;
 				_bpMatchFrameOffsets.setVal(it.frameOffset, nullptr);
 			}
+		} else if (it.type == kBreakpointProperty) {
+			_bpCheckPropRead |= it.varRead;
+			_bpCheckPropWrite |= it.varWrite;
 		} else if (it.type == kBreakpointVariable) {
 			_bpCheckVarRead |= it.varRead;
 			_bpCheckVarWrite |= it.varWrite;
@@ -1223,6 +1267,40 @@ void Debugger::builtinHook(const Symbol &funcSym) {
 	bpTest(builtinMatch);
 }
 
+void Debugger::propReadHook(const Common::String &name) {
+	if (name.empty())
+		return;
+	if (_bpCheckPropRead) {
+		for (auto &it : _breakpoints) {
+			if (it.type == kBreakpointProperty && it.varRead && it.varName.equalsIgnoreCase(name)) {
+				debugPrintf("Hit a breakpoint:\n");
+				debugPrintf("%s\n", it.format().c_str());
+				cmdScriptFrame(0, nullptr);
+				attach();
+				g_system->updateScreen();
+				break;
+			}
+		}
+	}
+}
+
+void Debugger::propWriteHook(const Common::String &name) {
+	if (name.empty())
+		return;
+	if (_bpCheckPropWrite) {
+		for (auto &it : _breakpoints) {
+			if (it.type == kBreakpointProperty && it.varWrite && it.varName.equalsIgnoreCase(name)) {
+				debugPrintf("Hit a breakpoint:\n");
+				debugPrintf("%s\n", it.format().c_str());
+				cmdScriptFrame(0, nullptr);
+				attach();
+				g_system->updateScreen();
+				break;
+			}
+		}
+	}
+}
+
 void Debugger::varReadHook(const Common::String &name) {
 	if (name.empty())
 		return;
diff --git a/engines/director/debugger.h b/engines/director/debugger.h
index c75cb3caa69..95900f2229b 100644
--- a/engines/director/debugger.h
+++ b/engines/director/debugger.h
@@ -39,6 +39,7 @@ enum BreakpointType {
 	kBreakpointVariable = 4,
 	kBreakpointEntity = 5,
 	kBreakpointEvent = 6,
+	kBreakpointProperty = 7,
 };
 
 struct Breakpoint {
@@ -74,6 +75,8 @@ public:
 	void pushContextHook();
 	void popContextHook();
 	void builtinHook(const Symbol &funcSym);
+	void propReadHook(const Common::String &varName);
+	void propWriteHook(const Common::String &varName);
 	void varReadHook(const Common::String &varName);
 	void varWriteHook(const Common::String &varName);
 	void entityReadHook(int entity, int field);
@@ -107,6 +110,7 @@ private:
 	bool cmdBpMovie(int argc, const char **argv);
 	bool cmdBpFrame(int argc, const char **argv);
 	bool cmdBpEntity(int argc, const char **argv);
+	bool cmdBpProp(int argc, const char **argv);
 	bool cmdBpVar(int argc, const char **argv);
 	bool cmdBpEvent(int argc, const char **argv);
 	bool cmdBpDel(int argc, const char **argv);
@@ -149,6 +153,8 @@ private:
 	Common::String _bpMatchMoviePath;
 	Common::HashMap<uint, void *> _bpMatchFuncOffsets;
 	Common::HashMap<uint, void *> _bpMatchFrameOffsets;
+	bool _bpCheckPropRead;
+	bool _bpCheckPropWrite;
 	bool _bpCheckVarRead;
 	bool _bpCheckVarWrite;
 	bool _bpCheckEntityRead;
diff --git a/engines/director/lingo/lingo-bytecode.cpp b/engines/director/lingo/lingo-bytecode.cpp
index 94889525c5a..5feeebd3c83 100644
--- a/engines/director/lingo/lingo-bytecode.cpp
+++ b/engines/director/lingo/lingo-bytecode.cpp
@@ -26,6 +26,7 @@
 
 #include "director/director.h"
 #include "director/cast.h"
+#include "director/debugger.h"
 #include "director/movie.h"
 #include "director/window.h"
 #include "director/castmember/castmember.h"
@@ -595,6 +596,7 @@ void LC::cb_theassign() {
 	if (g_lingo->_state->me.type == OBJECT) {
 		// Don't bother checking if the property is defined, leave that to the object.
 		// For D3-style anonymous objects/factories, you are allowed to define whatever properties you like.
+		g_debugger->propWriteHook(name);
 		g_lingo->_state->me.u.obj->setProp(name, value);
 	} else {
 		warning("cb_theassign: no me object");
@@ -622,6 +624,7 @@ void LC::cb_thepush() {
 	if (g_lingo->_state->me.type == OBJECT) {
 		if (g_lingo->_state->me.u.obj->hasProp(name)) {
 			g_lingo->push(g_lingo->_state->me.u.obj->getProp(name));
+			g_debugger->propReadHook(name);
 			return;
 		}
 
diff --git a/engines/director/lingo/lingo-the.cpp b/engines/director/lingo/lingo-the.cpp
index 3609c23249f..4caf2409952 100644
--- a/engines/director/lingo/lingo-the.cpp
+++ b/engines/director/lingo/lingo-the.cpp
@@ -1946,6 +1946,7 @@ void Lingo::getObjectProp(Datum &obj, Common::String &propName) {
 			g_lingo->lingoError("Lingo::getObjectProp: Object <%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 == PARRAY) {
@@ -1954,6 +1955,7 @@ void Lingo::getObjectProp(Datum &obj, Common::String &propName) {
 			d = obj.u.parr->arr[index - 1].v;
 		}
 		g_lingo->push(d);
+		g_debugger->propReadHook(propName);
 		return;
 	}
 	if (obj.type == RECT) {
@@ -1969,6 +1971,7 @@ void Lingo::getObjectProp(Datum &obj, Common::String &propName) {
 			g_lingo->lingoError("Lingo::getObjectProp: Rect <%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 == CASTREF) {
@@ -2065,6 +2068,7 @@ void Lingo::setObjectProp(Datum &obj, Common::String &propName, Datum &val) {
 	if (obj.type == OBJECT) {
 		if (obj.u.obj->hasProp(propName)) {
 			obj.u.obj->setProp(propName, val);
+			g_debugger->propWriteHook(propName);
 		} else {
 			g_lingo->lingoError("Lingo::setObjectProp: Object <%s> has no property '%s'", obj.asString(true).c_str(), propName.c_str());
 		}
@@ -2076,6 +2080,7 @@ void Lingo::setObjectProp(Datum &obj, Common::String &propName, Datum &val) {
 			PCell cell = PCell(propName, val);
 			obj.u.parr->arr.push_back(cell);
 		}
+		g_debugger->propWriteHook(propName);
 	} else if (obj.type == CASTREF) {
 		Movie *movie = _vm->getCurrentMovie();
 		if (!movie) {


Commit: b1e4e92ed2b238bfb049fdab63f0ed520e05ff12
    https://github.com/scummvm/scummvm/commit/b1e4e92ed2b238bfb049fdab63f0ed520e05ff12
Author: Scott Percival (code at moral.net.au)
Date: 2024-01-30T23:30:11+01:00

Commit Message:
DIRECTOR: Remove channel copying bodge

It is unclear what problem it was intended to solve, but it no longer
works now that 04cd1dccf9458e75fb146bffe1545fbfa5ed1862 has fixed the
frame loader to not reload if the frame hasn't changed.

Fixes movie playback in The Dark Eye.

Changed paths:
    engines/director/score.cpp


diff --git a/engines/director/score.cpp b/engines/director/score.cpp
index c03350e519d..64ea4211189 100644
--- a/engines/director/score.cpp
+++ b/engines/director/score.cpp
@@ -397,9 +397,6 @@ void Score::update() {
 		return;
 	}
 
-	for (uint ch = 0; ch < _channels.size(); ch++)
-		*_currentFrame->_sprites[ch] = *_channels[ch]->_sprite;
-
 	uint32 nextFrameNumberToLoad = _curFrameNumber;
 
 	if (!_vm->_playbackPaused) {


Commit: 5e18b53e93020552790de89178eecbc8b8824f21
    https://github.com/scummvm/scummvm/commit/5e18b53e93020552790de89178eecbc8b8824f21
Author: Scott Percival (code at moral.net.au)
Date: 2024-01-30T23:30:11+01:00

Commit Message:
DIRECTOR: Fix use-after-free in EXE loader

Changed paths:
    engines/director/resource.cpp


diff --git a/engines/director/resource.cpp b/engines/director/resource.cpp
index f93317cb2ed..a1e32599056 100644
--- a/engines/director/resource.cpp
+++ b/engines/director/resource.cpp
@@ -277,6 +277,8 @@ Archive *DirectorEngine::loadEXE(const Common::Path &movie) {
 	if (initialTag == MKTAG('R', 'I', 'F', 'X') || initialTag == MKTAG('X', 'F', 'I', 'R')) {
 		// we've encountered a movie saved from Director, not a projector.
 		result = loadEXERIFX(exeStream, 0);
+		// ownership handed to loadEXERIFX
+		exeStream = nullptr;
 	} else if (initialTag == MKTAG('R', 'I', 'F', 'F') || initialTag == MKTAG('F', 'F', 'I', 'R')) { // This is just a normal movie
 		result = new RIFFArchive();
 
@@ -285,6 +287,8 @@ Archive *DirectorEngine::loadEXE(const Common::Path &movie) {
 			delete result;
 			return nullptr;
 		}
+		// ownership handed to RIFFArchive
+		exeStream = nullptr;
 	} else {
 		Common::WinResources *exe = Common::WinResources::createFromEXE(movie);
 		if (!exe) {
@@ -328,17 +332,13 @@ Archive *DirectorEngine::loadEXE(const Common::Path &movie) {
 			delete exeStream;
 			return nullptr;
 		}
-
-		if (result) {
-			result->setPathName(movie);
-		}
-
-		return result;
+		// ownership passed to an EXE loader
+		exeStream = nullptr;
 	}
 
 	if (result)
 		result->setPathName(movie);
-	else
+	else if (exeStream)
 		delete exeStream;
 
 	return result;


Commit: bfb10b71878977abf097772abf2477276940a85a
    https://github.com/scummvm/scummvm/commit/bfb10b71878977abf097772abf2477276940a85a
Author: Scott Percival (code at moral.net.au)
Date: 2024-01-30T23:30:11+01:00

Commit Message:
DIRECTOR: Fix bounds of kTransPushUp

Fixes artifacting when changing the dials in scene03 of Team Xtreme:
Operation Weather Disaster.

Changed paths:
    engines/director/transitions.cpp


diff --git a/engines/director/transitions.cpp b/engines/director/transitions.cpp
index b39e93f2cc6..f0385ca5228 100644
--- a/engines/director/transitions.cpp
+++ b/engines/director/transitions.cpp
@@ -390,6 +390,8 @@ void Window::playTransition(uint frame, uint16 transDuration, uint8 transArea, u
 
 		case kTransPushUp:									// 14
 			rto.translate(0, h - t.yStepSize * i);
+			rfrom.bottom -= h - clipRect.findIntersectingRect(rto).height();
+			rto.clip(clipRect);
 			_composeSurface->blitFrom(nextFrame, rfrom, Common::Point(rto.left, rto.top));
 
 			rfrom.translate(0, t.yStepSize * i);


Commit: 2d1094a091d0597a765704f18076072c2eb0f4d5
    https://github.com/scummvm/scummvm/commit/2d1094a091d0597a765704f18076072c2eb0f4d5
Author: Scott Percival (code at moral.net.au)
Date: 2024-01-30T23:30:11+01:00

Commit Message:
DIRECTOR: LINGO: Allow reversed bound args to b_spriteBox

Fixes the targeting lines in the storm chasing minigame of Team Xtreme:
Operation Weather Disaster.

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


diff --git a/engines/director/channel.cpp b/engines/director/channel.cpp
index 06ea905cc62..95ccd6a0f3e 100644
--- a/engines/director/channel.cpp
+++ b/engines/director/channel.cpp
@@ -559,7 +559,7 @@ void Channel::replaceSprite(Sprite *nextSprite) {
 }
 
 void Channel::setWidth(int w) {
-	if (!(_sprite->_cast && _sprite->_cast->_type == kCastShape) && !_sprite->_stretch)
+	if (!(_sprite->_stretch || (_sprite->_cast && _sprite->_cast->_type == kCastShape)))
 		return;
 	_width = MAX<int>(w, 0);
 
@@ -568,7 +568,7 @@ void Channel::setWidth(int w) {
 }
 
 void Channel::setHeight(int h) {
-	if (!(_sprite->_cast && _sprite->_cast->_type == kCastShape) && !_sprite->_stretch)
+	if (!(_sprite->_stretch || (_sprite->_cast && _sprite->_cast->_type == kCastShape)))
 		return;
 	_height = MAX<int>(h, 0);
 
@@ -577,7 +577,7 @@ void Channel::setHeight(int h) {
 }
 
 void Channel::setBbox(int l, int t, int r, int b) {
-	if (!(_sprite->_cast && _sprite->_cast->_type == kCastShape) && !_sprite->_stretch)
+	if (!(_sprite->_stretch || (_sprite->_cast && _sprite->_cast->_type == kCastShape)))
 		return;
 	_width = r - l;
 	_height = b - t;
diff --git a/engines/director/lingo/lingo-builtins.cpp b/engines/director/lingo/lingo-builtins.cpp
index 989d4495b37..b9215037af6 100644
--- a/engines/director/lingo/lingo-builtins.cpp
+++ b/engines/director/lingo/lingo-builtins.cpp
@@ -2711,11 +2711,16 @@ void LB::b_spriteBox(int nargs) {
 	if (!channel)
 		return;
 
-	// This automatically sets the sctretch mode
+	// This automatically sets the stretch mode
 	channel->_sprite->_stretch = true;
 
 	g_director->getCurrentWindow()->addDirtyRect(channel->getBbox());
-	channel->setBbox(l, t, r, b);
+	channel->setBbox(
+		l < r ? l : r,
+		t < b ? t : b,
+		r > l ? r : l,
+		b > t ? b : t
+	);
 	if (channel->_sprite->_cast)
 		channel->_sprite->_cast->setModified(true);
 	channel->_dirty = true;


Commit: 27f0e8f8bb81d0bb8466159a0d43af9c528c1c8e
    https://github.com/scummvm/scummvm/commit/27f0e8f8bb81d0bb8466159a0d43af9c528c1c8e
Author: Scott Percival (code at moral.net.au)
Date: 2024-01-30T23:30:11+01:00

Commit Message:
DIRECTOR: Rework event ordering to be closer to original

Confirmed to work with:
- darkeye (race condition when viewing cutscenes or switching scenes in
  the menu)
- gadget (race condition after climbing the stairs in GA28)
- the7colors (intro cutscene timed with b_delay)
- wallobee (hotspot select gated with b_delay)

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


diff --git a/engines/director/lingo/lingo-builtins.cpp b/engines/director/lingo/lingo-builtins.cpp
index b9215037af6..b8533c81070 100644
--- a/engines/director/lingo/lingo-builtins.cpp
+++ b/engines/director/lingo/lingo-builtins.cpp
@@ -1401,8 +1401,7 @@ void LB::b_nothing(int nargs) {
 
 void LB::b_delay(int nargs) {
 	Datum d = g_lingo->pop();
-	g_director->getCurrentMovie()->getScore()->_nextFrameTime = g_system->getMillis() + (float)d.asInt() / 60 * 1000;
-	debugC(5, kDebugLoading, "b_delay(): delaying %d ticks, next frame time at %d", d.asInt(), g_director->getCurrentMovie()->getScore()->_nextFrameTime);
+	g_director->getCurrentMovie()->getScore()->setDelay(d.asInt());
 }
 
 void LB::b_do(int nargs) {
diff --git a/engines/director/lingo/lingo-events.cpp b/engines/director/lingo/lingo-events.cpp
index f9b3d95d499..4fa2840be86 100644
--- a/engines/director/lingo/lingo-events.cpp
+++ b/engines/director/lingo/lingo-events.cpp
@@ -92,7 +92,7 @@ ScriptType Lingo::event2script(LEvent ev) {
 		switch (ev) {
 		//case kEventStartMovie: // We are precompiling it now
 		//	return kMovieScript;
-		case kEventEnterFrame:
+		case kEventExitFrame:
 			return kScoreScript;
 		default:
 			return kNoneScript;
@@ -172,15 +172,18 @@ void Movie::queueFrameEvent(Common::Queue<LingoEvent> &queue, LEvent event, int
 	if (!script)
 		return;
 
-	// Scopeless statements (ie one lined lingo commands) are executed at enterFrame
-	// A score script can have both scopeless and scoped lingo. (eg. porting from D3.1 to D4)
-	if (event == kEventEnterFrame && script->_eventHandlers.contains(kEventGeneric)) {
-		queue.push(LingoEvent(kEventGeneric, eventId, kScoreScript, scriptId, true, 0));
-	}
-
 	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.
+
+	if (event == kEventExitFrame && script->_eventHandlers.contains(kEventGeneric) &&
+		!(script->_eventHandlers.contains(kEventExitFrame) || script->_eventHandlers.contains(kEventEnterFrame))) {
+		queue.push(LingoEvent(kEventGeneric, eventId, kScoreScript, scriptId, false, 0));
+	}
+
 }
 
 void Movie::queueMovieEvent(Common::Queue<LingoEvent> &queue, LEvent event, int eventId) {
@@ -261,7 +264,7 @@ void Movie::queueEvent(Common::Queue<LingoEvent> &queue, LEvent event, int targe
 			}
 			break;
 
-		case kEventEnterFrame:
+		case kEventExitFrame:
 			queueFrameEvent(queue, event, eventId);
 			break;
 
diff --git a/engines/director/lingo/lingo.cpp b/engines/director/lingo/lingo.cpp
index 00c53a7eeee..4656119fd44 100644
--- a/engines/director/lingo/lingo.cpp
+++ b/engines/director/lingo/lingo.cpp
@@ -1502,6 +1502,7 @@ void Lingo::executePerFrameHook(int frame, int subframe) {
 			for (uint i = 0; i < _actorList.u.farr->arr.size(); i++) {
 				Datum actor = _actorList.u.farr->arr[i];
 				Symbol method = actor.u.obj->getMethod("stepFrame");
+				debugC(1, kDebugLingoExec, "Executing perFrameHook : <%s>, frame %d, subframe %d", actor.asString(true).c_str(), frame, subframe);
 				if (method.nargs == 1)
 					push(actor);
 				LC::call(method, method.nargs, false);
diff --git a/engines/director/score.cpp b/engines/director/score.cpp
index 64ea4211189..343785b5498 100644
--- a/engines/director/score.cpp
+++ b/engines/director/score.cpp
@@ -72,6 +72,7 @@ Score::Score(Movie *movie) {
 	_nextFrame = 0;
 	_currentLabel = 0;
 	_nextFrameTime = 0;
+	_nextFrameDelay = 0;
 	_lastTempo = 0;
 	_waitForChannel = 0;
 	_waitForVideoChannel = 0;
@@ -273,6 +274,7 @@ void Score::startPlay() {
 	_curFrameNumber = 1;
 	_playState = kPlayStarted;
 	_nextFrameTime = 0;
+	_nextFrameDelay = 0;
 
 	if (!_currentFrame) {
 		warning("Score::startLoop(): Movie has no frames");
@@ -323,80 +325,48 @@ void Score::stopPlay() {
 	_lingo->executePerFrameHook(-1, 0);
 }
 
-void Score::update() {
-	if (_activeFade) {
-		if (!_soundManager->fadeChannel(_activeFade))
-			_activeFade = 0;
+void Score::setDelay(uint32 ticks) {
+	// the score will continually loop at the exitFrame handler,
+	// even if the handler sets a new delay value only the first one
+	// will be acknowledged.
+	if (!_nextFrameDelay) {
+		_nextFrameDelay = g_system->getMillis() + (ticks * 1000 / 60);
+		debugC(5, kDebugLoading, "Score::setDelay(): delaying %d ticks, next frame time at %d", ticks, _nextFrameDelay);
 	}
+}
 
-	if (!debugChannelSet(-1, kDebugFast)) {
-		bool keepWaiting = false;
+bool Score::isWaitingForNextFrame() {
+	bool keepWaiting = false;
 
-		debugC(8, kDebugLoading, "Score::update(): nextFrameTime: %d, time: %d", _nextFrameTime, g_system->getMillis(false));
-		if (_waitForChannel) {
-			if (_soundManager->isChannelActive(_waitForChannel)) {
-				keepWaiting = true;
-			} else {
-				_waitForChannel = 0;
-			}
-		} else if (_waitForClick) {
-			if (g_system->getMillis() >= _nextFrameTime + 1000) {
-				_waitForClickCursor = !_waitForClickCursor;
-				renderCursor(_movie->getWindow()->getMousePos());
-				_nextFrameTime = g_system->getMillis();
-			}
-			keepWaiting = true;
-		} else if (_waitForVideoChannel) {
-			Channel *movieChannel = _channels[_waitForVideoChannel];
-			if (movieChannel->isActiveVideo() && movieChannel->_movieRate != 0.0) {
-				keepWaiting = true;
-			} else {
-				_waitForVideoChannel = 0;
-			}
-		} else if (g_system->getMillis() < _nextFrameTime) {
+	debugC(8, kDebugLoading, "Score::isWaitingForNextFrame(): nextFrameTime: %d, time: %d", _nextFrameTime, g_system->getMillis(false));
+	if (_waitForChannel) {
+		if (_soundManager->isChannelActive(_waitForChannel)) {
 			keepWaiting = true;
+		} else {
+			_waitForChannel = 0;
 		}
-
-		if (keepWaiting) {
-			if (_movie->_videoPlayback) {
-				updateWidgets(true);
-				_window->render();
-			}
-
-			// Don't process frozen script if we use jump instructions
-			// like "go to frame", or open a new movie.
-			if (!_nextFrame || _nextFrame == _curFrameNumber) {
-				processFrozenScripts();
-			}
-
-			return;
-		}
-	}
-
-	// For previous frame
-	if (!_window->_newMovieStarted && !_vm->_playbackPaused) {
-		// When Lingo::func_goto* is called, _nextFrame is set
-		// and _skipFrameAdvance is set to true.
-		// exitFrame is not called in this case.
-		if (!_vm->_skipFrameAdvance && _vm->getVersion() >= 400) {
-			_movie->processEvent(kEventExitFrame);
+	} else if (_waitForClick) {
+		if (g_system->getMillis() >= _nextFrameTime + 1000) {
+			_waitForClickCursor = !_waitForClickCursor;
+			renderCursor(_movie->getWindow()->getMousePos());
+			_nextFrameTime = g_system->getMillis();
 		}
-
-		// If there is a transition, the perFrameHook is called
-		// after each transition subframe instead.
-		if (_currentFrame->_mainChannels.transType == 0 && _currentFrame->_mainChannels.trans.isNull()) {
-			_lingo->executePerFrameHook(_curFrameNumber, 0);
+		keepWaiting = true;
+	} else if (_waitForVideoChannel) {
+		Channel *movieChannel = _channels[_waitForVideoChannel];
+		if (movieChannel->isActiveVideo() && movieChannel->_movieRate != 0.0) {
+			keepWaiting = true;
+		} else {
+			_waitForVideoChannel = 0;
 		}
+	} else if (g_system->getMillis() < _nextFrameTime) {
+		keepWaiting = true;
 	}
 
-	_vm->_skipFrameAdvance = false;
-
-	// the exitFrame event handler may have stopped this movie
-	if (_playState == kPlayStopped) {
-		processFrozenScripts();
-		return;
-	}
+	return keepWaiting;
+}
 
+void Score::updateCurrentFrame() {
 	uint32 nextFrameNumberToLoad = _curFrameNumber;
 
 	if (!_vm->_playbackPaused) {
@@ -420,7 +390,6 @@ void Score::update() {
 				_playState = kPlayStopped;
 				window->setNextMovie(ref.movie);
 				window->_nextMovie.frameI = ref.frameI;
-				processFrozenScripts();
 				return;
 			}
 			nextFrameNumberToLoad = ref.frameI;
@@ -444,10 +413,13 @@ void Score::update() {
 	}
 
 	if (_curFrameNumber != nextFrameNumberToLoad) {
-		// this updates _curFrameNumber
+		// this copies in the frame data and updates _curFrameNumber
 		loadFrame(nextFrameNumberToLoad, true);
 	}
+	return;
+}
 
+void Score::updateNextFrameTime() {
 	byte tempo = _currentFrame->_mainChannels.scoreCachedTempo;
 	// puppetTempo is overridden by changes in score tempo
 	if (_currentFrame->_mainChannels.tempo || tempo != _lastTempo) {
@@ -469,34 +441,34 @@ void Score::update() {
 		if (tempo >= 256 - maxDelay) {
 			// Delay
 			_nextFrameTime = g_system->getMillis() + (256 - tempo) * 1000;
-			debugC(5, kDebugLoading, "Score::update(): setting _nextFrameTime to %d based on a delay of %d", _nextFrameTime, 256 - tempo);
+			debugC(5, kDebugLoading, "Score::updateNextFrameTime(): setting _nextFrameTime to %d based on a delay of %d", _nextFrameTime, 256 - tempo);
 		} else if (tempo <= 120) {
 			// FPS
 			_currentFrameRate = tempo;
 			if (g_director->_fpsLimit)
 				_currentFrameRate = MIN(g_director->_fpsLimit, _currentFrameRate);
 			_nextFrameTime = g_system->getMillis() + 1000.0 / (float)_currentFrameRate;
-			debugC(5, kDebugLoading, "Score::update(): setting _nextFrameTime to %d based on a framerate of %d", _nextFrameTime, _currentFrameRate);
+			debugC(5, kDebugLoading, "Score::updateNextFrameTime(): setting _nextFrameTime to %d based on a framerate of %d", _nextFrameTime, _currentFrameRate);
 		} else {
 			if (tempo == 128) {
 				_waitForClick = true;
 				_waitForClickCursor = false;
 				renderCursor(_movie->getWindow()->getMousePos());
-				debugC(5, kDebugLoading, "Score::update(): waiting for mouse click before next frame");
+				debugC(5, kDebugLoading, "Score::updateNextFrameTime(): waiting for mouse click before next frame");
 			} else if (!waitForClickOnly && tempo == 135) {
 				// Wait for sound channel 1
 				_waitForChannel = 1;
-				debugC(5, kDebugLoading, "Score::update(): waiting for sound channel 1 before next frame");
+				debugC(5, kDebugLoading, "Score::updateNextFrameTime(): waiting for sound channel 1 before next frame");
 			} else if (!waitForClickOnly && tempo == 134) {
 				// Wait for sound channel 2
 				_waitForChannel = 2;
-				debugC(5, kDebugLoading, "Score::update(): waiting for sound channel 2 before next frame");
+				debugC(5, kDebugLoading, "Score::updateNextFrameTime(): waiting for sound channel 2 before next frame");
 			} else if (!waitForClickOnly && tempo >= 136 && tempo <= 135 + _numChannelsDisplayed) {
 				// Wait for a digital video in a channel to finish playing
 				_waitForVideoChannel = tempo - 135;
-				debugC(5, kDebugLoading, "Score::update(): waiting for video in channel %d before next frame", _waitForVideoChannel);
+				debugC(5, kDebugLoading, "Score::updateNextFrameTime(): waiting for video in channel %d before next frame", _waitForVideoChannel);
 			} else {
-				warning("Unhandled tempo instruction: %d", tempo);
+				warning("Score::updateNextFrameTime(): Unhandled tempo instruction: %d", tempo);
 			}
 			_nextFrameTime = g_system->getMillis();
 		}
@@ -507,11 +479,115 @@ void Score::update() {
 
 	if (debugChannelSet(-1, kDebugSlow))
 		_nextFrameTime += 1000;
+}
+
+void Score::update() {
+	if (_activeFade) {
+		if (!_soundManager->fadeChannel(_activeFade))
+			_activeFade = 0;
+	}
+
+	if (!debugChannelSet(-1, kDebugFast)) {
+		// end update cycle if we're still waiting for the next frame
+		if (isWaitingForNextFrame()) {
+			if (_movie->_videoPlayback) {
+				updateWidgets(true);
+				_window->render();
+			}
+
+			// Don't process frozen script if we use jump instructions
+			// like "go to frame", or open a new movie.
+			if (!_nextFrame || _nextFrame == _curFrameNumber) {
+				processFrozenScripts();
+			}
+			return;
+		}
+	}
+
+	// For previous frame
+	if (!_window->_newMovieStarted && !_vm->_playbackPaused) {
+		// When Lingo::func_goto* is called, _nextFrame is set
+		// and _skipFrameAdvance is set to true.
+		// exitFrame is not called in this case.
+		if (!_vm->_skipFrameAdvance) {
+			// Exit the current frame. This can include scopeless ScoreScripts.
+			_movie->processEvent(kEventExitFrame);
+		}
+	}
+
+	_vm->_skipFrameAdvance = false;
+
+	// Check for delay
+	if (g_system->getMillis() < _nextFrameDelay) {
+		if (_movie->_videoPlayback) {
+			updateWidgets(true);
+			_window->render();
+		}
+
+		// Don't process frozen script if we use jump instructions
+		// like "go to frame", or open a new movie.
+		if (!_nextFrame || _nextFrame == _curFrameNumber) {
+			processFrozenScripts();
+		}
+
+		return;
+	}
+	_nextFrameDelay = 0;
+
+	// the exitFrame event handler may have stopped this movie
+	if (_playState == kPlayStopped) {
+		processFrozenScripts();
+		return;
+	}
+
+	// change current frame and load frame data, if required
+	updateCurrentFrame();
+
+	// set the delay time/condition until the next frame
+	updateNextFrameTime();
 
 	debugC(1, kDebugLoading, "******************************  Current frame: %d, time: %d", _curFrameNumber, g_system->getMillis(false));
 	g_debugger->frameHook();
 
-	_lingo->executeImmediateScripts(_currentFrame);
+	// movie could have been stopped by a window switch or a debug flag
+	if (_playState == kPlayStopped) {
+		processFrozenScripts();
+		return;
+	}
+
+	uint32 count = _window->frozenLingoStateCount();
+
+	// new frame, first call the perFrameHook (if one exists)
+	if (!_window->_newMovieStarted && !_vm->_playbackPaused) {
+		// Call the perFrameHook as soon as a frame switch is done.
+		// If there is a transition, the perFrameHook is called
+		// after each transition subframe instead of here.
+		if (_currentFrame->_mainChannels.transType == 0 && _currentFrame->_mainChannels.trans.isNull()) {
+			_lingo->executePerFrameHook(_curFrameNumber, 0);
+		}
+	}
+	if (_window->frozenLingoStateCount() > count)
+		return;
+
+	// check to see if we've hit the recursion limit
+	if (_vm->getVersion() >= 400 && _window->frozenLingoStateCount() >= 2) {
+		debugC(1, kDebugLoading, "Score::update(): hitting depth limit for D4 scripts, defrosting");
+		processFrozenScripts();
+		return;
+	} else if (_window->frozenLingoStateCount() >= 64) {
+		warning("Score::update(): Stopping runaway script recursion. By this point D3 will have run out of stack space");
+		processFrozenScripts();
+		return;
+	}
+
+	// then call the stepMovie hook (if one exists)
+	// skip the first frame
+	count = _window->frozenLingoStateCount();
+	if (!_window->_newMovieStarted && !_vm->_playbackPaused) {
+		_movie->processEvent(kEventStepMovie);
+	}
+	if (_window->frozenLingoStateCount() > count)
+		return;
 
 	if (_vm->getVersion() >= 600) {
 		// _movie->processEvent(kEventBeginSprite);
@@ -524,36 +600,19 @@ void Score::update() {
 	renderFrame(_curFrameNumber);
 	_window->_newMovieStarted = false;
 
-	// Enter and exit from previous frame
-	if (!_vm->_playbackPaused) {
-		uint32 count = _window->frozenLingoStateCount();
-		// Triggers the frame script in D2-3, explicit enterFrame handlers in D4+
-		// D4 will only process recursive enterFrame handlers to a depth of 2.
-		// Any more will be ignored.
-		if ((_vm->getVersion() >= 400)) {
-			if (count < 2 || _window->recursiveEnterFrameCount() < 2)
-				_movie->processEvent(kEventEnterFrame);
-			else {
-				warning("Score::update(): ignoring recursive enterFrame handler, frozenLingoStateCount: %d, enterFrames: %d", count, _window->recursiveEnterFrameCount());
-			}
-		} else if ((_vm->getVersion() < 400) || _movie->_allowOutdatedLingo) {
-			// Force a flush of any frozen scripts before raising enterFrame
-			if (!processFrozenScripts())
-				return;
-			_movie->processEvent(kEventEnterFrame);
-			if ((_vm->getVersion() >= 300) || _movie->_allowOutdatedLingo) {
-				// Movie version of enterFrame, for D3 only. The D3 Interactivity Manual claims
-				// "This handler executes before anything else when the playback head moves."
-				// but this is incorrect. The frame script is executed first.
-				_movie->processEvent(kEventStepMovie);
-			}
-		}
-		// If another frozen state gets triggered, wait another update() before thawing
-		if ((_vm->getVersion() < 400) && _window->frozenLingoStateCount() > 0)
-			return;
-		else if ((_vm->getVersion() >= 400) && _window->frozenLingoStateCount() > count)
-			return;
+	// then call the enterFrame hook (if one exists)
+	count = _window->frozenLingoStateCount();
+	if (!_vm->_playbackPaused && _vm->getVersion() >= 400) {
+		_movie->processEvent(kEventEnterFrame);
 	}
+	if (_window->frozenLingoStateCount() > count)
+		return;
+
+	// then execute any immediate scripts, i.e. handlers attached to sprites
+	count = _window->frozenLingoStateCount();
+	_lingo->executeImmediateScripts(_currentFrame);
+	if (_window->frozenLingoStateCount() > count)
+		return;
 
 	// Attempt to thaw and continue any frozen execution after startMovie and enterFrame.
 	// If they don't complete (i.e. another freezing event like a "go to frame"),
diff --git a/engines/director/score.h b/engines/director/score.h
index c988dec2289..3ce859c3d45 100644
--- a/engines/director/score.h
+++ b/engines/director/score.h
@@ -93,6 +93,7 @@ public:
 	void startPlay();
 	void step();
 	void stopPlay();
+	void setDelay(uint32 ticks);
 
 	void setCurrentFrame(uint16 frameId) { _nextFrame = frameId; }
 	uint16 getCurrentFrameNum() { return _curFrameNumber; }
@@ -137,6 +138,9 @@ public:
 	Common::String formatChannelInfo();
 
 private:
+	bool isWaitingForNextFrame();
+	void updateCurrentFrame();
+	void updateNextFrameTime();
 	void update();
 	void playQueuedSound();
 
@@ -175,6 +179,7 @@ public:
 
 	PlayState _playState;
 	uint32 _nextFrameTime;
+	uint32 _nextFrameDelay;
 	int _lastTempo;
 	int _waitForChannel;
 	int _waitForVideoChannel;


Commit: b50ed6ace42c19bc9747d3573061bd3e4dc27d71
    https://github.com/scummvm/scummvm/commit/b50ed6ace42c19bc9747d3573061bd3e4dc27d71
Author: Scott Percival (code at moral.net.au)
Date: 2024-01-30T23:30:11+01:00

Commit Message:
DIRECTOR: Don't render transitions when movie is paused

Fixes rendering of menu screen on Cosmology of Kyoto.

Changed paths:
    engines/director/score.cpp


diff --git a/engines/director/score.cpp b/engines/director/score.cpp
index 343785b5498..44310df538a 100644
--- a/engines/director/score.cpp
+++ b/engines/director/score.cpp
@@ -644,6 +644,9 @@ void Score::renderFrame(uint16 frameId, RenderMode mode) {
 	if (_skipTransition) {
 		_window->render();
 		_skipTransition = false;
+	} else if (g_director->_playbackPaused) {
+		renderSprites(frameId, mode);
+		_window->render();
 	} else if (!renderTransition(frameId)) {
 		bool skip = renderPrePaletteCycle(frameId, mode);
 		setLastPalette(frameId);


Commit: 5d69024aa438c4c5cf56b957d40fa23d3325ceee
    https://github.com/scummvm/scummvm/commit/5d69024aa438c4c5cf56b957d40fa23d3325ceee
Author: Scott Percival (code at moral.net.au)
Date: 2024-01-30T23:30:11+01:00

Commit Message:
DIRECTOR: Clean up Score methods that previously needed frameId

Changed paths:
    engines/director/channel.cpp
    engines/director/channel.h
    engines/director/lingo/lingo-builtins.cpp
    engines/director/lingo/lingo-the.cpp
    engines/director/lingo/xlibs/unittest.cpp
    engines/director/score.cpp
    engines/director/score.h
    engines/director/transitions.cpp


diff --git a/engines/director/channel.cpp b/engines/director/channel.cpp
index 95ccd6a0f3e..cdf585b5d09 100644
--- a/engines/director/channel.cpp
+++ b/engines/director/channel.cpp
@@ -378,7 +378,7 @@ void Channel::setCast(CastMemberID memberID) {
 	_sprite->setAutoPuppet(kAPCast, true);
 }
 
-void Channel::setClean(Sprite *nextSprite, int spriteId, bool partial) {
+void Channel::setClean(Sprite *nextSprite, bool partial) {
 	if (!nextSprite)
 		return;
 
diff --git a/engines/director/channel.h b/engines/director/channel.h
index ab01d24ad0d..ace25307bf8 100644
--- a/engines/director/channel.h
+++ b/engines/director/channel.h
@@ -62,7 +62,7 @@ public:
 	void setBbox(int l, int t, int r, int b);
 	void setPosition(int x, int y, bool force = false);
 	void setCast(CastMemberID memberID);
-	void setClean(Sprite *nextSprite, int spriteId, bool partial = false);
+	void setClean(Sprite *nextSprite, bool partial = false);
 	bool getEditable();
 	void setEditable(bool editable);
 	void replaceSprite(Sprite *nextSprite);
diff --git a/engines/director/lingo/lingo-builtins.cpp b/engines/director/lingo/lingo-builtins.cpp
index b8533c81070..7c466b83c32 100644
--- a/engines/director/lingo/lingo-builtins.cpp
+++ b/engines/director/lingo/lingo-builtins.cpp
@@ -2823,7 +2823,7 @@ void LB::b_updateStage(int nargs) {
 	movie->getWindow()->render();
 
 	// play any puppet sounds that have been queued
-	score->playSoundChannel(score->getCurrentFrameNum(), true);
+	score->playSoundChannel(true);
 
 	if (score->_cursorDirty) {
 		score->renderCursor(movie->getWindow()->getMousePos());
diff --git a/engines/director/lingo/lingo-the.cpp b/engines/director/lingo/lingo-the.cpp
index 4caf2409952..87359ab1277 100644
--- a/engines/director/lingo/lingo-the.cpp
+++ b/engines/director/lingo/lingo-the.cpp
@@ -1155,7 +1155,7 @@ void Lingo::setTheEntity(int entity, Datum &id, int field, Datum &d) {
 		g_director->getCurrentWindow()->setStageColor(g_director->transformColor(d.asInt()));
 
 		// Redraw the stage
-		score->renderSprites(score->getCurrentFrameNum(), kRenderForceUpdate);
+		score->renderSprites(kRenderForceUpdate);
 		g_director->getCurrentWindow()->render();
 		break;
 	case kTheSwitchColorDepth:
@@ -1644,7 +1644,7 @@ void Lingo::setTheSprite(Datum &id1, int field, Datum &d) {
 		break;
 	case kTheRect:
 		if (d.type == RECT || (d.type == ARRAY && d.u.farr->arr.size() >= 4)) {
-			score->renderSprites(score->getCurrentFrameNum(), kRenderForceUpdate);
+			score->renderSprites(kRenderForceUpdate);
 			channel->setBbox(
 				d.u.farr->arr[0].u.i, d.u.farr->arr[1].u.i,
 				d.u.farr->arr[2].u.i, d.u.farr->arr[3].u.i
diff --git a/engines/director/lingo/xlibs/unittest.cpp b/engines/director/lingo/xlibs/unittest.cpp
index a2fe7bfe0c2..17fc5683fa0 100644
--- a/engines/director/lingo/xlibs/unittest.cpp
+++ b/engines/director/lingo/xlibs/unittest.cpp
@@ -86,7 +86,7 @@ void UnitTest::m_UTScreenshot(int nargs) {
 
 	// force a full screen redraw before taking the screenshot
 	Score *score = g_director->getCurrentMovie()->getScore();
-	score->renderSprites(score->getCurrentFrameNum(), kRenderForceUpdate);
+	score->renderSprites(kRenderForceUpdate);
 	Window *window = g_director->getCurrentWindow();
 	window->render();
 	Graphics::ManagedSurface *windowSurface = window->getSurface();
diff --git a/engines/director/score.cpp b/engines/director/score.cpp
index 44310df538a..84f436fd1c3 100644
--- a/engines/director/score.cpp
+++ b/engines/director/score.cpp
@@ -645,19 +645,19 @@ void Score::renderFrame(uint16 frameId, RenderMode mode) {
 		_window->render();
 		_skipTransition = false;
 	} else if (g_director->_playbackPaused) {
-		renderSprites(frameId, mode);
+		renderSprites(mode);
 		_window->render();
 	} else if (!renderTransition(frameId)) {
-		bool skip = renderPrePaletteCycle(frameId, mode);
-		setLastPalette(frameId);
-		renderSprites(frameId, mode);
+		bool skip = renderPrePaletteCycle(mode);
+		setLastPalette();
+		renderSprites(mode);
 		_window->render();
 		if (!skip)
-			renderPaletteCycle(frameId, mode);
+			renderPaletteCycle(mode);
 	}
 
 
-	playSoundChannel(frameId, false);
+	playSoundChannel(false);
 	playQueuedSound(); // this is currently only used in FPlayXObj
 
 	if (_cursorDirty) {
@@ -673,20 +673,20 @@ bool Score::renderTransition(uint16 frameId) {
 	TransParams *tp = _window->_puppetTransition;
 
 	if (tp) {
-		setLastPalette(frameId);
+		setLastPalette();
 		_window->playTransition(frameId, tp->duration, tp->area, tp->chunkSize, tp->type, currentFrame->_mainChannels.scoreCachedPaletteId);
 		delete _window->_puppetTransition;
 		_window->_puppetTransition = nullptr;
 		return true;
 	} else if (currentFrame->_mainChannels.transType) {
-		setLastPalette(frameId);
+		setLastPalette();
 		_window->playTransition(frameId, currentFrame->_mainChannels.transDuration, currentFrame->_mainChannels.transArea, currentFrame->_mainChannels.transChunkSize, currentFrame->_mainChannels.transType, currentFrame->_mainChannels.scoreCachedPaletteId);
 		return true;
 	} else if (!currentFrame->_mainChannels.trans.isNull()) {
 		CastMember *member = _movie->getCastMember(currentFrame->_mainChannels.trans);
 		if (member && member->_type == kCastTransition) {
 			TransitionCastMember *trans = static_cast<TransitionCastMember *>(member);
-			setLastPalette(frameId);
+			setLastPalette();
 			_window->playTransition(frameId, trans->_durationMillis, trans->_area, trans->_chunkSize, trans->_transType, currentFrame->_mainChannels.scoreCachedPaletteId);
 			return true;
 		}
@@ -694,7 +694,7 @@ bool Score::renderTransition(uint16 frameId) {
 	return false;
 }
 
-void Score::renderSprites(uint16 frameId, RenderMode mode) {
+void Score::renderSprites(RenderMode mode) {
 	if (_window->_newMovieStarted)
 		mode = kRenderForceUpdate;
 
@@ -726,7 +726,7 @@ void Score::renderSprites(uint16 frameId, RenderMode mode) {
 				nextSprite->setCast(nextSprite->_castId);
 			}
 
-			channel->setClean(nextSprite, i);
+			channel->setClean(nextSprite);
 			// Check again to see if a video has just been started by setClean.
 			if (channel->isActiveVideo())
 				_movie->_videoPlayback = true;
@@ -742,7 +742,7 @@ void Score::renderSprites(uint16 frameId, RenderMode mode) {
 				debugC(5, kDebugImages, "Score::renderSprites(): CH: %-3d: No sprite", i);
 			}
 		} else {
-			channel->setClean(nextSprite, i, true);
+			channel->setClean(nextSprite, true);
 		}
 
 		// update editable text channel after we render the sprites. because for the current frame, we may get those sprites only when we finished rendering
@@ -752,7 +752,7 @@ void Score::renderSprites(uint16 frameId, RenderMode mode) {
 	}
 }
 
-bool Score::renderPrePaletteCycle(uint16 frameId, RenderMode mode) {
+bool Score::renderPrePaletteCycle(RenderMode mode) {
 	if (_puppetPalette)
 		return false;
 
@@ -868,7 +868,7 @@ bool Score::renderPrePaletteCycle(uint16 frameId, RenderMode mode) {
 	return false;
 }
 
-void Score::setLastPalette(uint16 frameId) {
+void Score::setLastPalette() {
 	if (_puppetPalette)
 		return;
 
@@ -909,7 +909,7 @@ bool Score::isPaletteColorCycling() {
 	return _currentFrame->_mainChannels.palette.colorCycling;
 }
 
-void Score::renderPaletteCycle(uint16 frameId, RenderMode mode) {
+void Score::renderPaletteCycle(RenderMode mode) {
 	if (_puppetPalette)
 		return;
 
@@ -1438,7 +1438,7 @@ Channel *Score::getChannelById(uint16 id) {
 	return _channels[id];
 }
 
-void Score::playSoundChannel(uint16 frameId, bool puppetOnly) {
+void Score::playSoundChannel(bool puppetOnly) {
 	debugC(5, kDebugSound, "playSoundChannel(): Sound1 %s Sound2 %s", _currentFrame->_mainChannels.sound1.asString().c_str(), _currentFrame->_mainChannels.sound2.asString().c_str());
 	DirectorSound *sound = _window->getSoundManager();
 
diff --git a/engines/director/score.h b/engines/director/score.h
index 3ce859c3d45..ad52b7159ec 100644
--- a/engines/director/score.h
+++ b/engines/director/score.h
@@ -123,17 +123,17 @@ public:
 
 	bool renderTransition(uint16 frameId);
 	void renderFrame(uint16 frameId, RenderMode mode = kRenderModeNormal);
-	void renderSprites(uint16 frameId, RenderMode mode = kRenderModeNormal);
-	bool renderPrePaletteCycle(uint16 frameId, RenderMode mode = kRenderModeNormal);
-	void setLastPalette(uint16 frameId);
+	void renderSprites(RenderMode mode = kRenderModeNormal);
+	bool renderPrePaletteCycle(RenderMode mode = kRenderModeNormal);
+	void setLastPalette();
 	bool isPaletteColorCycling();
-	void renderPaletteCycle(uint16 frameId, RenderMode mode = kRenderModeNormal);
+	void renderPaletteCycle(RenderMode mode = kRenderModeNormal);
 	void renderCursor(Common::Point pos, bool forceUpdate = false);
 	void updateWidgets(bool hasVideoPlayback);
 
 	void invalidateRectsForMember(CastMember *member);
 
-	void playSoundChannel(uint16 frameId, bool puppetOnly);
+	void playSoundChannel(bool puppetOnly);
 
 	Common::String formatChannelInfo();
 
diff --git a/engines/director/transitions.cpp b/engines/director/transitions.cpp
index f0385ca5228..2e6a00e19a3 100644
--- a/engines/director/transitions.cpp
+++ b/engines/director/transitions.cpp
@@ -191,7 +191,7 @@ void Window::playTransition(uint frame, uint16 transDuration, uint8 transArea, u
 	Score *score = g_director->getCurrentMovie()->getScore();
 	if (t.area) {
 		// Changed area transition
-		score->renderSprites(t.frame);
+		score->renderSprites();
 
 		if (_dirtyRects.size() == 0)
 			return;
@@ -217,7 +217,7 @@ void Window::playTransition(uint frame, uint16 transDuration, uint8 transArea, u
 		render(false, &nextFrame);
 	} else {
 		// Full stage transition
-		score->renderSprites(t.frame, kRenderForceUpdate);
+		score->renderSprites(kRenderForceUpdate);
 		render(true, &nextFrame);
 
 		clipRect = _innerDims;


Commit: c4fe1add55e6ad7224b6c667b2bda0c23e1160b1
    https://github.com/scummvm/scummvm/commit/c4fe1add55e6ad7224b6c667b2bda0c23e1160b1
Author: Scott Percival (code at moral.net.au)
Date: 2024-01-30T23:30:11+01:00

Commit Message:
DIRECTOR: Persist editable status on sprites if cast is the same

Fixes text box deselecting when talking to the cops in Spaceship
Warlock.

Changed paths:
    engines/director/channel.cpp


diff --git a/engines/director/channel.cpp b/engines/director/channel.cpp
index cdf585b5d09..0016522e01c 100644
--- a/engines/director/channel.cpp
+++ b/engines/director/channel.cpp
@@ -530,6 +530,12 @@ void Channel::replaceSprite(Sprite *nextSprite) {
 		((DigitalVideoCastMember *)_sprite->_cast)->rewindVideo();
 	}
 
+	// If the cast member is the same, persist the editable flag
+	bool editable = nextSprite->_editable;
+	if (_sprite->_castId == nextSprite->_castId) {
+		editable = _sprite->_editable;
+	}
+
 	int width = _width;
 	int height = _height;
 	bool immediate = _sprite->_immediate;
@@ -539,6 +545,8 @@ void Channel::replaceSprite(Sprite *nextSprite) {
 	// Persist the immediate flag
 	_sprite->_immediate = immediate;
 
+	_sprite->_editable = editable;
+
 	// TODO: auto expand text size is meaning less for us, not all text
 	// since we are using initialRect for the text cast member now, then the sprite size is meaning less for us.
 	// thus, we keep the _sprite size here




More information about the Scummvm-git-logs mailing list