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

sev- noreply at scummvm.org
Mon Mar 16 13:28:45 UTC 2026


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

Summary:
36d44f732e DIRECTOR: Add detection table entry for finklemania
80782f8764 DIRECTOR: Copy back _behaviors and _spriteInfo from frameData
6292b5d822 DIRECTOR: LINGO: Disable compareTo element-to-array coercion in D6
47afa441ba DIRECTOR: Fix dimension typo in mask bounds check
da505aa4f1 DIRECTOR: Track which sprite fields are updated by a frame swap
c57821cd18 DIRECTOR: Remove channel offset in D2 FilmLoop loader


Commit: 36d44f732e1a0d830d9e935f3d6511b78d41d573
    https://github.com/scummvm/scummvm/commit/36d44f732e1a0d830d9e935f3d6511b78d41d573
Author: Scott Percival (code at moral.net.au)
Date: 2026-03-16T14:28:38+01:00

Commit Message:
DIRECTOR: Add detection table entry for finklemania

Changed paths:
    engines/director/detection_tables.h


diff --git a/engines/director/detection_tables.h b/engines/director/detection_tables.h
index 6d75d7590d1..edad044a6b9 100644
--- a/engines/director/detection_tables.h
+++ b/engines/director/detection_tables.h
@@ -172,6 +172,7 @@ static const PlainGameDescriptor directorGames[] = {
 	{ "fff",				"Four Footed Friends" },
 	{ "finegardening",		"Fine Gardening Propagation" },
 	{ "finkletimes",		"Professor Finkle's Times Table Factory" },
+	{ "finklemania",		"Professor Finkle's Math Mania" },
 	{ "fishwish",			"The Fish Who Could Wish" },
 	{ "flipper",			"Flipper" },
 	{ "flipper1",           "The Three Worlds of Flipper & Lopaka" },
@@ -8504,6 +8505,11 @@ static const DirectorGameDescription gameDescriptions[] = {
 	MACGAME1("exploder", "", "Winblows Exploder", "r:089658f6d7d68f07eece8a68e5fdc35e",  115391, 600),
 	WINGAME1("exploder", "", "EXPLODER.EXE",	  "t:ae488c615d7ecb621c96c59b98eccf9b", 4712625, 600),
 
+	WINGAME2f("finklemania", "CD 1", "MANIA.EXE",	  "t:870ffc676c55b1fb7b1b489ce6d17f3e", 1708689,
+			"MAIN.DXR", "f:f16e0cf9386e52d8bd961ddf12c56a69", 7034800, 600, GF_32BPP),
+	WINGAME2f("finklemania", "CD 2", "MANIA.EXE",	  "t:870ffc676c55b1fb7b1b489ce6d17f3e", 1708689,
+			"MAIN.DXR", "f:027b5108850dd528bcee19c2a5ef4624", 7047084, 600, GF_32BPP),
+
 	MACGAME1("flingshot", "", "FlingShot", "769cbb727ae2dda3f8ab6682d65ab439", 1031461, 650),
 
 	MACGAME1_l("forestia", "", "FORESTIA", "r:3c0d928e84acac2ee6739a4360e70fc6", 1032406, Common::FR_FRA, 602),


Commit: 80782f87645329869259d026d4392af3c96c2e71
    https://github.com/scummvm/scummvm/commit/80782f87645329869259d026d4392af3c96c2e71
Author: Scott Percival (code at moral.net.au)
Date: 2026-03-16T14:28:38+01:00

Commit Message:
DIRECTOR: Copy back _behaviors and _spriteInfo from frameData

Fixes most BehaviorScript interactions in D6.

Changed paths:
    engines/director/sprite.cpp


diff --git a/engines/director/sprite.cpp b/engines/director/sprite.cpp
index ba710450836..cf112de52e2 100644
--- a/engines/director/sprite.cpp
+++ b/engines/director/sprite.cpp
@@ -617,6 +617,9 @@ void Sprite::replaceFrom(Sprite *nextSprite) {
 		return;
 
 	_scriptId = nextSprite->_scriptId;
+	// Copy all the behavior scripts
+	_behaviors = nextSprite->_behaviors;
+	_spriteInfo = nextSprite->_spriteInfo;
 
 	if (_puppet) {
 		// Whole sprite is in puppet mode.


Commit: 6292b5d822b9280030bd0b32c02780bd1957fafe
    https://github.com/scummvm/scummvm/commit/6292b5d822b9280030bd0b32c02780bd1957fafe
Author: Scott Percival (code at moral.net.au)
Date: 2026-03-16T14:28:38+01:00

Commit Message:
DIRECTOR: LINGO: Disable compareTo element-to-array coercion in D6

It appears Macromedia fixed this very wacky behaviour, and in D6
we only need to compare arrays with other arrays.

Fixes movie switching script in Professor Finkle's Math Mania.

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 ce709b81cc8..afd1a64a041 100644
--- a/engines/director/lingo/lingo-code.cpp
+++ b/engines/director/lingo/lingo-code.cpp
@@ -1324,6 +1324,11 @@ Datum LC::compareArrays(Datum (*compareFunc)(Datum, Datum), Datum d1, Datum d2,
 	// At least one of d1 and d2 must be an array
 	bool d1isArr = d1.isArray() || d1.type == PARRAY;
 	bool d2isArr = d2.isArray() || d2.type == PARRAY;
+	// As far as I can tell, D6 no longer does partial array or element-to-array comparison
+	if ((g_director->getVersion() >= 600) && (!(d1isArr && d2isArr))) {
+		return Datum(0);
+	}
+
 	uint32 d1size = d1.isArray() ? d1.u.farr->arr.size() : d1.type == PARRAY ? d1.u.parr->arr.size() : 0;
 	uint32 d2size = d2.isArray() ? d2.u.farr->arr.size() : d2.type == PARRAY ? d2.u.parr->arr.size() : 0;
 	// The calling convention of this checking function is a bit weird:
diff --git a/engines/director/lingo/tests/equality.lingo b/engines/director/lingo/tests/equality.lingo
index ea8c1492604..c671a639100 100644
--- a/engines/director/lingo/tests/equality.lingo
+++ b/engines/director/lingo/tests/equality.lingo
@@ -142,6 +142,17 @@ scummvmAssert(not([1, 2, 3] <> [1, "2", 3.0]))
 scummvmAssert(["testa", "testb"] = ["testa", "TESTB"])
 scummvmAssert([#a: "testa", #b: "testb"] = [#a: "testa", #b: "TESTB"])
 
+scummvmAssert(("" = []))
+scummvmAssert(("sausages" = []))
+scummvmAssert(not("" = ["sausages"]))
+scummvmAssert(("sausages" = ["sausages"]))
+
+set the scummvmVersion to 600
+scummvmAssert(not("" = []))
+scummvmAssert(not("sausages" = []))
+scummvmAssert(not("" = ["sausages"]))
+scummvmAssert(not("sausages" = ["sausages"]))
+
 -- D4 has a quirk where only the left side list elements are checked
 set the scummvmVersion to 400
 scummvmAssert([] = [1, "2", 4])


Commit: 47afa441ba94725c38d7d344bfd177eba57d24f8
    https://github.com/scummvm/scummvm/commit/47afa441ba94725c38d7d344bfd177eba57d24f8
Author: Scott Percival (code at moral.net.au)
Date: 2026-03-16T14:28:38+01:00

Commit Message:
DIRECTOR: Fix dimension typo in mask bounds check

Changed paths:
    engines/director/graphics.cpp


diff --git a/engines/director/graphics.cpp b/engines/director/graphics.cpp
index b8c26b769eb..77b858c6d8f 100644
--- a/engines/director/graphics.cpp
+++ b/engines/director/graphics.cpp
@@ -763,7 +763,7 @@ void DirectorPlotData::inkBlitSurface(Common::Rect &srcRect, const Graphics::Sur
 		const byte *msk = mask ? (const byte *)mask->getBasePtr(srcPoint.x, srcPoint.y) : nullptr;
 
 		if (srfMask) {
-			if (srcPoint.x >= srfMask->w)
+			if (srcPoint.y >= srfMask->h)
 				continue;
 
 			msk = (const byte *)srfMask->getBasePtr(srcPoint.x, srcPoint.y);
@@ -776,7 +776,7 @@ void DirectorPlotData::inkBlitSurface(Common::Rect &srcRect, const Graphics::Sur
 			}
 
 			// Do not try render beyond the mask bounds
-			if (srfMask && (srcPoint.y >= srfMask->h))
+			if (srfMask && (srcPoint.x >= srfMask->w))
 				continue;
 
 			if (!(mask || srfMask) || (msk && (*msk++))) {


Commit: da505aa4f11fadc9aa60d9a42328e140ec1b8998
    https://github.com/scummvm/scummvm/commit/da505aa4f11fadc9aa60d9a42328e140ec1b8998
Author: Scott Percival (code at moral.net.au)
Date: 2026-03-16T14:28:38+01:00

Commit Message:
DIRECTOR: Track which sprite fields are updated by a frame swap

We need to be able to apply only delta changes from the score data,
while at the same time having a fully-formed copy that we can
revert to if the channel gets the puppet flag disabled.

Previously, if a channel was unchanged in the score over two frames,
and the puppet flag wasn't set, and the cast ID was changed by a script
in the first frame, this wasn't being persisted. Now we only copy back
the sprite fields we know for a fact have changed.

Fixes card animations in Gus Goes to the Kooky Carnival.

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


diff --git a/engines/director/frame.cpp b/engines/director/frame.cpp
index b6d72c559d4..30b07ae9dc3 100644
--- a/engines/director/frame.cpp
+++ b/engines/director/frame.cpp
@@ -329,21 +329,27 @@ void readSpriteDataD2(Common::SeekableReadStreamEndian &stream, Sprite &sprite,
 		switch (stream.pos() - startPosition) {
 		case 0:
 			sprite._scriptId = CastMemberID(stream.readByte(), DEFAULT_CAST_LIB);
+			sprite._copyBackMask |= kSCBCastId;
 			break;
 		case 1:
 			sprite._spriteType = (SpriteType)stream.readByte();
 			sprite._enabled = sprite._spriteType != kInactiveSprite;
+			sprite._copyBackMask |= kSCBSpriteType;
+			sprite._copyBackMask |= kSCBEnabled;
 			break;
 		case 2:
 			// Normalize D2 and D3 colors from -128 ... 127 to 0 ... 255.
 			sprite._foreColor = g_director->transformColor(stream.readByte() ^ 0x80);
+			sprite._copyBackMask |= kSCBForeColor;
 			break;
 		case 3:
 			// Normalize D2 and D3 colors from -128 ... 127 to 0 ... 255.
 			sprite._backColor = g_director->transformColor(stream.readByte() ^ 0x80);
+			sprite._copyBackMask |= kSCBBackColor;
 			break;
 		case 4:
 			sprite._thickness = stream.readByte() & 0x7f;
+			sprite._copyBackMask |= kSCBThickness;
 			break;
 		case 5:
 			sprite._inkData = stream.readByte();
@@ -351,6 +357,7 @@ void readSpriteDataD2(Common::SeekableReadStreamEndian &stream, Sprite &sprite,
 			sprite._ink = static_cast<InkType>(sprite._inkData & 0x3f);
 			sprite._trails = sprite._inkData & 0x40 ? true : false;
 			sprite._stretch = sprite._inkData & 0x80 ? true : false;
+			sprite._copyBackMask |= kSCBInk;
 			break;
 		case 6:
 			if (sprite.isQDShape()) {
@@ -360,18 +367,23 @@ void readSpriteDataD2(Common::SeekableReadStreamEndian &stream, Sprite &sprite,
 				sprite._pattern = 0;
 				sprite._castId = CastMemberID(stream.readUint16(), DEFAULT_CAST_LIB);
 			}
+			sprite._copyBackMask |= kSCBCastId;
 			break;
 		case 8:
 			sprite._startPoint.y = (int16)stream.readUint16();
+			sprite._copyBackMask |= kSCBStartPoint;
 			break;
 		case 10:
 			sprite._startPoint.x = (int16)stream.readUint16();
+			sprite._copyBackMask |= kSCBStartPoint;
 			break;
 		case 12:
 			sprite._height = (int16)stream.readUint16();
+			sprite._copyBackMask |= kSCBHeight;
 			break;
 		case 14:
 			sprite._width = (int16)stream.readUint16();
+			sprite._copyBackMask |= kSCBWidth;
 			break;
 		default:
 			// This means that a `case` label has to be split at this position
@@ -647,15 +659,20 @@ void readSpriteDataD4(Common::SeekableReadStreamEndian &stream, Sprite &sprite,
 		case 1:
 			sprite._spriteType = (SpriteType)stream.readByte();
 			sprite._enabled = sprite._spriteType != kInactiveSprite;
+			sprite._copyBackMask |= kSCBSpriteType;
+			sprite._copyBackMask |= kSCBEnabled;
 			break;
 		case 2:
 			sprite._foreColor = g_director->transformColor((uint8)stream.readByte());
+			sprite._copyBackMask |= kSCBForeColor;
 			break;
 		case 3:
 			sprite._backColor = g_director->transformColor((uint8)stream.readByte());
+			sprite._copyBackMask |= kSCBBackColor;
 			break;
 		case 4:
 			sprite._thickness = stream.readByte();
+			sprite._copyBackMask |= kSCBThickness;
 			break;
 		case 5:
 			sprite._inkData = stream.readByte();
@@ -663,28 +680,36 @@ void readSpriteDataD4(Common::SeekableReadStreamEndian &stream, Sprite &sprite,
 			sprite._ink = static_cast<InkType>(sprite._inkData & 0x3f);
 			sprite._trails = sprite._inkData & 0x40 ? true : false;
 			sprite._stretch = sprite._inkData & 0x80 ? true : false;
+			sprite._copyBackMask |= kSCBInk;
 			break;
 		case 6:
 			if (sprite.isQDShape()) {
 				sprite._pattern = stream.readUint16();
+				sprite._copyBackMask |= kSCBPattern;
 			} else {
 				sprite._castId = CastMemberID(stream.readUint16(), DEFAULT_CAST_LIB);
+				sprite._copyBackMask |= kSCBCastId;
 			}
 			break;
 		case 8:
 			sprite._startPoint.y = (int16)stream.readUint16();
+			sprite._copyBackMask |= kSCBStartPoint;
 			break;
 		case 10:
 			sprite._startPoint.x = (int16)stream.readUint16();
+			sprite._copyBackMask |= kSCBStartPoint;
 			break;
 		case 12:
 			sprite._height = (int16)stream.readUint16();
+			sprite._copyBackMask |= kSCBHeight;
 			break;
 		case 14:
 			sprite._width = (int16)stream.readUint16();
+			sprite._copyBackMask |= kSCBWidth;
 			break;
 		case 16:
 			sprite._scriptId = CastMemberID(stream.readUint16(), DEFAULT_CAST_LIB);
+			sprite._copyBackMask |= kSCBScriptId;
 			break;
 		case 18:
 			// & 0x0f scorecolor
@@ -696,10 +721,11 @@ void readSpriteDataD4(Common::SeekableReadStreamEndian &stream, Sprite &sprite,
 
 			sprite._editable = ((sprite._colorcode & 0x40) == 0x40);
 			sprite._moveable = ((sprite._colorcode & 0x80) == 0x80);
-			sprite._moveable = ((sprite._colorcode & 0x80) == 0x80);
+			sprite._copyBackMask |= kSCBMoveable;
 			break;
 		case 19:
 			sprite._blendAmount = stream.readByte();
+			sprite._copyBackMask |= kSCBBlendAmount;
 			break;
 		default:
 			// This means that a `case` label has to be split at this position
@@ -967,6 +993,7 @@ void readSpriteDataD5(Common::SeekableReadStreamEndian &stream, Sprite &sprite,
 		switch (stream.pos() - startPosition) {
 		case 0:
 			sprite._spriteType = (SpriteType)stream.readByte();
+			sprite._copyBackMask |= kSCBSpriteType;
 			break;
 		case 1:
 			sprite._inkData = stream.readByte();
@@ -974,44 +1001,55 @@ void readSpriteDataD5(Common::SeekableReadStreamEndian &stream, Sprite &sprite,
 			sprite._ink = static_cast<InkType>(sprite._inkData & 0x3f);
 			sprite._trails = sprite._inkData & 0x40 ? true : false;
 			sprite._stretch = sprite._inkData & 0x80 ? true : false;
+			sprite._copyBackMask |= kSCBInk;
 			break;
 		case 2: {
 				int castLib = stream.readSint16();
 				sprite._castId = CastMemberID(sprite._castId.member, castLib);
+				sprite._copyBackMask |= kSCBCastId;
 			}
 			break;
 		case 4: {
 				uint16 memberID = stream.readUint16();
 				sprite._castId = CastMemberID(memberID, sprite._castId.castLib);  // Inherit castLib from previous frame
+				sprite._copyBackMask |= kSCBCastId;
 			}
 			break;
 		case 6: {
 				int scriptCastLib = stream.readSint16();
 				sprite._scriptId = CastMemberID(sprite._scriptId.member, scriptCastLib);
+				sprite._copyBackMask |= kSCBCastId;
 			}
 			break;
 		case 8: {
 				uint16 scriptMemberID = stream.readUint16();
 				sprite._scriptId = CastMemberID(scriptMemberID, sprite._scriptId.castLib);  // Inherit castLib from previous frame
+				sprite._copyBackMask |= kSCBScriptId;
 			}
 			break;
 		case 10:
 			sprite._foreColor = g_director->transformColor((uint8)stream.readByte());
+			sprite._copyBackMask |= kSCBForeColor;
 			break;
 		case 11:
 			sprite._backColor = g_director->transformColor((uint8)stream.readByte());
+			sprite._copyBackMask |= kSCBBackColor;
 			break;
 		case 12:
 			sprite._startPoint.y = (int16)stream.readUint16();
+			sprite._copyBackMask |= kSCBStartPoint;
 			break;
 		case 14:
 			sprite._startPoint.x = (int16)stream.readUint16();
+			sprite._copyBackMask |= kSCBStartPoint;
 			break;
 		case 16:
 			sprite._height = (int16)stream.readUint16();
+			sprite._copyBackMask |= kSCBHeight;
 			break;
 		case 18:
 			sprite._width = (int16)stream.readUint16();
+			sprite._copyBackMask |= kSCBWidth;
 			break;
 		case 20:
 			// & 0x0f scorecolor
@@ -1024,12 +1062,15 @@ void readSpriteDataD5(Common::SeekableReadStreamEndian &stream, Sprite &sprite,
 			sprite._editable = ((sprite._colorcode & 0x40) == 0x40);
 			sprite._moveable = ((sprite._colorcode & 0x80) == 0x80);
 			sprite._moveable = ((sprite._colorcode & 0x80) == 0x80);
+			sprite._copyBackMask |= kSCBMoveable;
 			break;
 		case 21:
 			sprite._blendAmount = stream.readByte();
+			sprite._copyBackMask |= kSCBBlendAmount;
 			break;
 		case 22:
 			sprite._thickness = stream.readByte();
+			sprite._copyBackMask |= kSCBThickness;
 			break;
 		case 23:
 			(void)stream.readByte(); // unused
@@ -1393,6 +1434,7 @@ void readSpriteDataD6(Common::SeekableReadStreamEndian &stream, Sprite &sprite,
 		case 0:
 			sprite._spriteType = (SpriteType)stream.readByte();
 			debugC(6, kDebugLoading, "    sprite._spriteType: %d", sprite._spriteType);
+			sprite._copyBackMask |= kSCBSpriteType;
 			break;
 		case 1:
 			sprite._inkData = stream.readByte();
@@ -1400,6 +1442,7 @@ void readSpriteDataD6(Common::SeekableReadStreamEndian &stream, Sprite &sprite,
 			sprite._ink = static_cast<InkType>(sprite._inkData & 0x3f);
 			sprite._trails = sprite._inkData & 0x40 ? true : false;
 			sprite._stretch = sprite._inkData & 0x80 ? true : false;
+			sprite._copyBackMask |= kSCBInk;
 
 			debugC(6, kDebugLoading, "    sprite._inkData: 0x%02x", sprite._inkData);
 			debugC(6, kDebugLoading, "    sprite._ink: %d", sprite._ink);
@@ -1407,52 +1450,58 @@ void readSpriteDataD6(Common::SeekableReadStreamEndian &stream, Sprite &sprite,
 			debugC(6, kDebugLoading, "    sprite._stretch: %d", sprite._stretch);
 			break;
 		case 2:
-			if (sprite._puppet || sprite.getAutoPuppet(kAPForeColor)) {
-				stream.readByte();
-			} else {
-				sprite._foreColor = g_director->transformColor((uint8)stream.readByte());
+			sprite._foreColor = g_director->transformColor((uint8)stream.readByte());
+			sprite._copyBackMask |= kSCBForeColor;
 
-				debugC(6, kDebugLoading, "    sprite._foreColor: 0x%02x", sprite._foreColor);
-			}
+			debugC(6, kDebugLoading, "    sprite._foreColor: 0x%02x", sprite._foreColor);
 			break;
 		case 3:
 			sprite._backColor = g_director->transformColor((uint8)stream.readByte());
+			sprite._copyBackMask |= kSCBBackColor;
 			debugC(6, kDebugLoading, "    sprite._backColor: 0x%02x", sprite._backColor);
 			break;
 		case 4: {
 				int castLib = stream.readSint16();
 				sprite._castId = CastMemberID(sprite._castId.member, castLib);
+				sprite._copyBackMask |= kSCBCastId;
 				debugC(6, kDebugLoading, "    sprite._castId: %d", sprite._castId.member);
 			}
 			break;
 		case 6: {
 				uint16 memberID = stream.readUint16();
 				sprite._castId = CastMemberID(memberID, sprite._castId.castLib);  // Inherit castLib from previous frame
+				sprite._copyBackMask |= kSCBCastId;
 				debugC(6, kDebugLoading, "    sprite._castId: %d", sprite._castId.member);
 			}
 			break;
 		case 8:
 			sprite._spriteListIdx = stream.readUint32();
+			sprite._copyBackMask |= kSCBSpriteListIdx;
 			debugC(6, kDebugLoading, "    sprite._spriteListIdx: %d", sprite._spriteListIdx);
 			break;
 		case 10: // This field could be optimized
 			sprite._spriteListIdx = stream.readUint16();
+			sprite._copyBackMask |= kSCBSpriteListIdx;
 			debugC(6, kDebugLoading, "    sprite._spriteListIdx (16): %d", sprite._spriteListIdx);
 			break;
 		case 12:
 			sprite._startPoint.y = (int16)stream.readUint16();
+			sprite._copyBackMask |= kSCBStartPoint;
 			debugC(6, kDebugLoading, "    sprite._startPoint.y: %d", sprite._startPoint.y);
 			break;
 		case 14:
 			sprite._startPoint.x = (int16)stream.readUint16();
+			sprite._copyBackMask |= kSCBStartPoint;
 			debugC(6, kDebugLoading, "    sprite._startPoint.x: %d", sprite._startPoint.x);
 			break;
 		case 16:
 			sprite._height = (int16)stream.readUint16();
+			sprite._copyBackMask |= kSCBHeight;
 			debugC(6, kDebugLoading, "    sprite._height: %d", sprite._height);
 			break;
 		case 18:
 			sprite._width = (int16)stream.readUint16();
+			sprite._copyBackMask |= kSCBWidth;
 			debugC(6, kDebugLoading, "    sprite._width: %d", sprite._width);
 			break;
 		case 20:
@@ -1466,6 +1515,7 @@ void readSpriteDataD6(Common::SeekableReadStreamEndian &stream, Sprite &sprite,
 			sprite._editable = ((sprite._colorcode & 0x40) == 0x40);
 			sprite._moveable = ((sprite._colorcode & 0x80) == 0x80);
 			sprite._moveable = ((sprite._colorcode & 0x80) == 0x80);
+			sprite._copyBackMask |= kSCBMoveable;
 
 			debugC(6, kDebugLoading, "    sprite._colorcode: 0x%02x", sprite._colorcode);
 			debugC(6, kDebugLoading, "    sprite._editable: %d", sprite._editable);
@@ -1473,10 +1523,12 @@ void readSpriteDataD6(Common::SeekableReadStreamEndian &stream, Sprite &sprite,
 			break;
 		case 21:
 			sprite._blendAmount = stream.readByte();
+			sprite._copyBackMask |= kSCBBlendAmount;
 			debugC(6, kDebugLoading, "    sprite._blendAmount: %d", sprite._blendAmount);
 			break;
 		case 22:
 			sprite._thickness = stream.readByte();
+			sprite._copyBackMask |= kSCBThickness;
 			debugC(6, kDebugLoading, "    sprite._thickness: %d", sprite._thickness);
 			break;
 		case 23:
@@ -1833,164 +1885,115 @@ void readSpriteDataD7(Common::SeekableReadStreamEndian &stream, Sprite &sprite,
 	while (stream.pos() < finishPosition) {
 		switch (stream.pos() - startPosition) {
 		case 0:
-			if (sprite._puppet) {
-				stream.readByte();
-			} else {
-				sprite._spriteType = (SpriteType)stream.readByte();
-			}
+			sprite._spriteType = (SpriteType)stream.readByte();
+			sprite._copyBackMask |= kSCBSpriteType;
 			break;
 		case 1:
-			if (sprite._puppet || sprite.getAutoPuppet(kAPInk)) {
-				stream.readByte();
-			} else {
-				sprite._inkData = stream.readByte();
+			sprite._inkData = stream.readByte();
 
-				sprite._ink = static_cast<InkType>(sprite._inkData & 0x3f);
-				sprite._trails = sprite._inkData & 0x40 ? true : false;
-				sprite._stretch = sprite._inkData & 0x80 ? true : false;
-			}
+			sprite._ink = static_cast<InkType>(sprite._inkData & 0x3f);
+			sprite._trails = sprite._inkData & 0x40 ? true : false;
+			sprite._stretch = sprite._inkData & 0x80 ? true : false;
+			sprite._copyBackMask |= kSCBInk;
 			break;
 		case 2:
-			if (sprite._puppet || sprite.getAutoPuppet(kAPForeColor)) {
-				stream.readByte();
-			} else {
-				sprite._foreColor = g_director->transformColor((uint8)stream.readByte());
-			}
+			sprite._foreColor = g_director->transformColor((uint8)stream.readByte());
+			sprite._copyBackMask |= kSCBForeColor;
 			break;
 		case 3:
-			if (sprite._puppet || sprite.getAutoPuppet(kAPBackColor)) {
-				stream.readByte();
-			} else {
-				sprite._backColor = g_director->transformColor((uint8)stream.readByte());
-			}
+			sprite._backColor = g_director->transformColor((uint8)stream.readByte());
+			sprite._copyBackMask |= kSCBBackColor;
 			break;
 		case 4:
-			if (sprite._puppet || sprite.getAutoPuppet(kAPCast)) {
-				stream.readSint16();
-			} else {
+			{
 				int castLib = stream.readSint16();
 				sprite._castId = CastMemberID(sprite._castId.member, castLib);
+				sprite._copyBackMask |= kSCBCastId;
 			}
 			break;
 		case 6:
-			if (sprite._puppet || sprite.getAutoPuppet(kAPCast)) {
-				stream.readUint16();
-			} else {
+			{
 				uint16 memberID = stream.readUint16();
 				sprite._castId = CastMemberID(memberID, sprite._castId.castLib);  // Inherit castLib from previous frame
+				sprite._copyBackMask |= kSCBCastId;
 			}
 			break;
 		case 8:
-			if (sprite._puppet || sprite.getAutoPuppet(kAPCast)) {
-				stream.readUint32();
-			} else {
-				sprite._spriteListIdx = stream.readUint32();
-			}
+			sprite._spriteListIdx = stream.readUint32();
+			sprite._copyBackMask |= kSCBSpriteListIdx;
 			break;
 		case 10: // This field could be optimized
-			if (sprite._puppet || sprite.getAutoPuppet(kAPCast)) {
-				stream.readUint16();
-			} else {
-				sprite._spriteListIdx = stream.readUint16();
-			}
+			sprite._spriteListIdx = stream.readUint16();
+			sprite._copyBackMask |= kSCBSpriteListIdx;
 			break;
 		case 12:
-			if (sprite._puppet || sprite.getAutoPuppet(kAPLoc)) {
-				stream.readUint16();
-			} else {
-				sprite._startPoint.y = (int16)stream.readUint16();
-			}
+			sprite._startPoint.y = (int16)stream.readUint16();
+			sprite._copyBackMask |= kSCBStartPoint;
 			break;
 		case 14:
-			if (sprite._puppet || sprite.getAutoPuppet(kAPLoc)) {
-				stream.readUint16();
-			} else {
-				sprite._startPoint.x = (int16)stream.readUint16();
-			}
+			sprite._startPoint.x = (int16)stream.readUint16();
+			sprite._copyBackMask |= kSCBStartPoint;
 			break;
 		case 16:
-			if (sprite._puppet || sprite.getAutoPuppet(kAPHeight)) {
-				stream.readUint16();
-			} else {
-				sprite._height = (int16)stream.readUint16();
-			}
+			sprite._height = (int16)stream.readUint16();
+			sprite._copyBackMask |= kSCBHeight;
 			break;
 		case 18:
-			if (sprite._puppet || sprite.getAutoPuppet(kAPWidth)) {
-				stream.readUint16();
-			} else {
-				sprite._width = (int16)stream.readUint16();
-			}
+			sprite._width = (int16)stream.readUint16();
+			sprite._copyBackMask |= kSCBWidth;
 			break;
 		case 20:
-			if (sprite._puppet || sprite.getAutoPuppet(kAPMoveable)) {
-				stream.readByte();
-			} else {
-				// & 0x0f scorecolor
-				// 0x10 forecolor is rgb
-				// 0x20 bgcolor is rgb
-				// 0x40 editable
-				// 0x80 moveable
-				sprite._colorcode = stream.readByte();
-
-				sprite._editable = ((sprite._colorcode & 0x40) == 0x40);
-				sprite._moveable = ((sprite._colorcode & 0x80) == 0x80);
-				sprite._moveable = ((sprite._colorcode & 0x80) == 0x80);
-			}
+			// & 0x0f scorecolor
+			// 0x10 forecolor is rgb
+			// 0x20 bgcolor is rgb
+			// 0x40 editable
+			// 0x80 moveable
+			sprite._colorcode = stream.readByte();
+
+			sprite._editable = ((sprite._colorcode & 0x40) == 0x40);
+			sprite._moveable = ((sprite._colorcode & 0x80) == 0x80);
+			sprite._moveable = ((sprite._colorcode & 0x80) == 0x80);
+			sprite._copyBackMask |= kSCBMoveable;
 			break;
 		case 21:
-			if (sprite._puppet) {
-				stream.readByte();
-			} else {
-				sprite._blendAmount = stream.readByte();
-			}
+			sprite._blendAmount = stream.readByte();
+			sprite._copyBackMask |= kSCBBlendAmount;
 			break;
 		case 22:
-			if (sprite._puppet) {
-				stream.readByte();
-			} else {
-				sprite._thickness = stream.readByte();
-			}
+			sprite._thickness = stream.readByte();
+			sprite._copyBackMask |= kSCBThickness;
 			break;
 		case 23:
 			sprite._flags = stream.readByte();
+			sprite._flags |= kSCBFlags;
 			break;
 		case 24:
-			if (sprite._puppet || sprite.getAutoPuppet(kAPForeColor)) {
-				stream.readByte();
-			} else {
-				sprite._fgColorG = (uint8)stream.readByte();
-			}
+			sprite._fgColorG = (uint8)stream.readByte();
+			sprite._flags |= kSCBForeColor;
 			break;
 		case 25:
-			if (sprite._puppet || sprite.getAutoPuppet(kAPBackColor)) {
-				stream.readByte();
-			} else {
-				sprite._bgColorG = (uint8)stream.readByte();
-			}
+			sprite._bgColorG = (uint8)stream.readByte();
+			sprite._flags |= kSCBBackColor;
 			break;
 		case 26:
-			if (sprite._puppet || sprite.getAutoPuppet(kAPForeColor)) {
-				stream.readByte();
-			} else {
-				sprite._fgColorB = (uint8)stream.readByte();
-			}
+			sprite._fgColorB = (uint8)stream.readByte();
+			sprite._flags |= kSCBForeColor;
 			break;
 		case 27:
-			if (sprite._puppet || sprite.getAutoPuppet(kAPBackColor)) {
-				stream.readByte();
-			} else {
-				sprite._bgColorB = (uint8)stream.readByte();
-			}
+			sprite._bgColorB = (uint8)stream.readByte();
+			sprite._flags |= kSCBBackColor;
 			break;
 		case 28:
 			sprite._angleRot = stream.readUint32();
+			sprite._flags |= kSCBAngle;
 			break;
 		case 30:	// half of the field
 			sprite._angleRot = stream.readUint16();
+			sprite._flags |= kSCBAngle;
 			break;
 		case 32:
 			sprite._angleSkew = stream.readUint32();
+			sprite._flags |= kSCBAngle;
 			break;
 		case 36:
 			stream.read(unk, 12); // alignment bytes
diff --git a/engines/director/score.cpp b/engines/director/score.cpp
index 1dd509dff5e..09e1c60a6f9 100644
--- a/engines/director/score.cpp
+++ b/engines/director/score.cpp
@@ -991,6 +991,12 @@ void Score::updateSprites(RenderMode mode, bool withClean) {
 	}
 
 	createScriptInstances(_curFrameNumber);
+
+	// We've updated the channels from the frame, reset the copyback mask so that e.g.
+	// disabling the puppet flag copies all the data as expected.
+	for (auto &it : _currentFrame->_sprites) {
+		it->_copyBackMask = kSCBNoMask;
+	}
 }
 
 bool Score::renderPrePaletteCycle(RenderMode mode) {
@@ -2132,6 +2138,11 @@ bool Score::loadFrame(int frameNum, bool loadCast) {
 			it->reset();
 	}
 
+	// Zero out the copyback mask on all sprites, so we know what changed
+	for (auto &it : _currentFrame->_sprites) {
+		it->_copyBackMask = 0;
+	}
+
 	debugC(7, kDebugLoading, "****** Source frame %d to Destination frame %d, current offset 0x%x", sourceFrame, targetFrame, (uint32)_framesStream->pos());
 
 	_curFrameNumber = sourceFrame + 1;
diff --git a/engines/director/sprite.cpp b/engines/director/sprite.cpp
index cf112de52e2..da48a88863a 100644
--- a/engines/director/sprite.cpp
+++ b/engines/director/sprite.cpp
@@ -45,6 +45,7 @@ Sprite::Sprite(Frame *frame) {
 }
 
 void Sprite::reset() {
+	_copyBackMask = kSCBNoMask;
 	_scriptId = CastMemberID(0, 0);
 	_colorcode = 0;
 	_blendAmount = 0;
@@ -91,6 +92,7 @@ Sprite& Sprite::operator=(const Sprite &sprite) {
 		return *this;
 	}
 
+	_copyBackMask = sprite._copyBackMask;
 	_frame = sprite._frame;
 	_score = sprite._score;
 	_movie = sprite._movie;
@@ -616,10 +618,13 @@ void Sprite::replaceFrom(Sprite *nextSprite) {
 	if (!nextSprite)
 		return;
 
-	_scriptId = nextSprite->_scriptId;
+	if (nextSprite->_copyBackMask & kSCBScriptId)
+		_scriptId = nextSprite->_scriptId;
 	// Copy all the behavior scripts
 	_behaviors = nextSprite->_behaviors;
 	_spriteInfo = nextSprite->_spriteInfo;
+	if (nextSprite->_copyBackMask & kSCBSpriteListIdx)
+		_spriteListIdx = nextSprite->_spriteListIdx;
 
 	if (_puppet) {
 		// Whole sprite is in puppet mode.
@@ -637,42 +642,57 @@ void Sprite::replaceFrom(Sprite *nextSprite) {
 
 	// Copy over all the sprite fields from one to another.
 	// For D6+, exclude individual fields with autopuppet switched on
-	_spriteType = nextSprite->_spriteType;
-	_enabled = nextSprite->_enabled;
-	if (!getAutoPuppet(kAPInk)) {
+	if (nextSprite->_copyBackMask & kSCBSpriteType)
+		_spriteType = nextSprite->_spriteType;
+
+	if (nextSprite->_copyBackMask & kSCBEnabled)
+		_enabled = nextSprite->_enabled;
+
+	if (!getAutoPuppet(kAPInk) && (nextSprite->_copyBackMask & kSCBInk)) {
 		_inkData = nextSprite->_inkData;
 		_ink = nextSprite->_ink;
 		_trails = nextSprite->_trails;
 		_stretch = nextSprite->_stretch;
 	}
-	if (!getAutoPuppet(kAPForeColor)) {
+	if (!getAutoPuppet(kAPForeColor) && (nextSprite->_copyBackMask & kSCBForeColor)) {
 		_foreColor = nextSprite->_foreColor;
+		_fgColorB = nextSprite->_fgColorB;
+		_fgColorG = nextSprite->_fgColorG;
 	}
-	if (!getAutoPuppet(kAPBackColor)) {
+	if (!getAutoPuppet(kAPBackColor) && (nextSprite->_copyBackMask & kSCBBackColor)) {
 		_backColor = nextSprite->_backColor;
+		_bgColorB = nextSprite->_bgColorB;
+		_bgColorG = nextSprite->_bgColorG;
 	}
 	if (!getAutoPuppet(kAPCast)) {
-		_castId = nextSprite->_castId;
-		_cast = nextSprite->_cast;
-		_spriteListIdx = nextSprite->_spriteListIdx;
+		if (nextSprite->_copyBackMask & kSCBCastId) {
+			_castId = nextSprite->_castId;
+			_cast = nextSprite->_cast;
+		}
+		if (nextSprite->_copyBackMask & kSCBSpriteListIdx)
+			_spriteListIdx = nextSprite->_spriteListIdx;
 	}
-	if (!getAutoPuppet(kAPLoc)) {
+	if (!getAutoPuppet(kAPLoc) && (nextSprite->_copyBackMask & kSCBStartPoint)) {
 		_startPoint = nextSprite->_startPoint;
 	}
-	if (!getAutoPuppet(kAPHeight)) {
+	// height and width seem to be copied back if the cast ID changes (e.g. intro of sabotenman)
+	if (!getAutoPuppet(kAPHeight) && ((nextSprite->_copyBackMask & kSCBCastId) || (nextSprite->_copyBackMask & kSCBHeight))) {
 		_height = nextSprite->_height;
 	}
-	if (!getAutoPuppet(kAPWidth)) {
+	if (!getAutoPuppet(kAPWidth) && ((nextSprite->_copyBackMask & kSCBCastId) || (nextSprite->_copyBackMask & kSCBWidth))) {
 		_width = nextSprite->_width;
 	}
-	if (!getAutoPuppet(kAPMoveable)) {
+	if (!getAutoPuppet(kAPMoveable) && (nextSprite->_copyBackMask & kSCBMoveable)) {
 		_colorcode = nextSprite->_colorcode;
 		_editable = nextSprite->_editable;
 		_moveable = nextSprite->_moveable;
 	}
-	_blendAmount = nextSprite->_blendAmount;
-	_thickness = nextSprite->_thickness;
-	_pattern = nextSprite->_pattern;
+	if (nextSprite->_copyBackMask & kSCBBlendAmount)
+		_blendAmount = nextSprite->_blendAmount;
+	if (nextSprite->_copyBackMask & kSCBThickness)
+		_thickness = nextSprite->_thickness;
+	if (nextSprite->_copyBackMask & kSCBPattern)
+		_pattern = nextSprite->_pattern;
 
 	// Persist the immediate flag
 	_immediate = immediate;
diff --git a/engines/director/sprite.h b/engines/director/sprite.h
index 3a467e8f69c..4155955e7b3 100644
--- a/engines/director/sprite.h
+++ b/engines/director/sprite.h
@@ -65,6 +65,38 @@ enum ThicknessFlags {
 	kTTweened   = 0x80,
 };
 
+// Director treats changes of sprites between score frames as deltas.
+// Only the delta is applied to what's on the screen.
+// If a sprite has the puppet flag, or a field has been autopuppeted,
+// then that will block the sprite/fields from being updated by the score.
+// In addition, the program can turn off the puppet flag at any time, which
+// will revert the sprite to whatever was in the score.
+
+// In order to keep a single frame read and copying pass, when reading the frame
+// data we keep track of what fields have changed, so that the frame can be
+// stored as a full copy but applied as a delta.
+
+enum SpriteCopyBackMask {
+	kSCBNoMask = -1,
+	kSCBScriptId = 0x00001,
+	kSCBSpriteType = 0x00002,
+	kSCBEnabled = 0x00004,
+	kSCBForeColor = 0x00008,
+	kSCBBackColor = 0x00010,
+	kSCBThickness = 0x00020,
+	kSCBInk = 0x00040,
+	kSCBPattern = 0x00080,
+	kSCBCastId = 0x00100,
+	kSCBStartPoint = 0x00200,
+	kSCBHeight = 0x00400,
+	kSCBWidth = 0x00800,
+	kSCBMoveable = 0x01000,
+	kSCBBlendAmount = 0x02000,
+	kSCBSpriteListIdx = 0x04000,
+	kSCBFlags = 0x08000,
+	kSCBAngle = 0x10000,
+};
+
 class Sprite {
 public:
 	Sprite(Frame *frame = nullptr);
@@ -119,6 +151,8 @@ public:
 
 	Graphics::FloodFill *_matte; // matte for quickdraw shape
 
+	uint32 _copyBackMask;
+
 	CastMemberID _scriptId;
 	byte _colorcode; // x40 editable, 0x80 moveable
 	byte _blendAmount;


Commit: c57821cd186bbb41d21841aecd1536b6189cba08
    https://github.com/scummvm/scummvm/commit/c57821cd186bbb41d21841aecd1536b6189cba08
Author: Scott Percival (code at moral.net.au)
Date: 2026-03-16T14:28:38+01:00

Commit Message:
DIRECTOR: Remove channel offset in D2 FilmLoop loader

It was arbitrary at the time, and we have an example of a game that
expects channel 0 to be writeable.

Fixes crash at DATA:R-E:HMD-V1 at 180 in L-Zone (FilmLoopCastMember 107).

Thanks to Mylstar Electronics and Cavey74 for the tipoff.

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


diff --git a/engines/director/castmember/filmloop.cpp b/engines/director/castmember/filmloop.cpp
index 2c0bd704592..d0a540b8cea 100644
--- a/engines/director/castmember/filmloop.cpp
+++ b/engines/director/castmember/filmloop.cpp
@@ -226,7 +226,7 @@ void FilmLoopCastMember::loadFilmLoopDataD2(Common::SeekableReadStreamEndian &st
 
 		while (frameSize > 0) {
 			int msgWidth = stream.readByte() * 2;
-			int order = stream.readByte() * 2 - 0x20;
+			int order = stream.readByte() * 2;
 			frameSize -= 2;
 
 			int channel = order / channelSize;
diff --git a/engines/director/frame.cpp b/engines/director/frame.cpp
index 30b07ae9dc3..d9261b8ee0a 100644
--- a/engines/director/frame.cpp
+++ b/engines/director/frame.cpp
@@ -324,7 +324,7 @@ void Frame::readSpriteD2(Common::MemoryReadStreamEndian &stream, uint16 offset,
 	}
 }
 
-void readSpriteDataD2(Common::SeekableReadStreamEndian &stream, Sprite &sprite, uint32 startPosition, uint32 finishPosition) {
+void readSpriteDataD2(Common::SeekableReadStreamEndian &stream, Sprite &sprite, int startPosition, int finishPosition) {
 	while (stream.pos() < finishPosition) {
 		switch (stream.pos() - startPosition) {
 		case 0:
@@ -647,7 +647,7 @@ void Frame::readSpriteD4(Common::MemoryReadStreamEndian &stream, uint16 offset,
 	}
 }
 
-void readSpriteDataD4(Common::SeekableReadStreamEndian &stream, Sprite &sprite, uint32 startPosition, uint32 finishPosition) {
+void readSpriteDataD4(Common::SeekableReadStreamEndian &stream, Sprite &sprite, int startPosition, int finishPosition) {
 	debugC(8, kDebugLoading, "stream.pos(): %0x, startPosition: %d, finishPosition: %d", (int)stream.pos(), startPosition, finishPosition);
 	while (stream.pos() < finishPosition) {
 		switch (stream.pos() - startPosition) {
@@ -988,7 +988,7 @@ void Frame::readSpriteD5(Common::MemoryReadStreamEndian &stream, uint16 offset,
 		sprite._width = sprite._height = 0;
 }
 
-void readSpriteDataD5(Common::SeekableReadStreamEndian &stream, Sprite &sprite, uint32 startPosition, uint32 finishPosition) {
+void readSpriteDataD5(Common::SeekableReadStreamEndian &stream, Sprite &sprite, int startPosition, int finishPosition) {
 	while (stream.pos() < finishPosition) {
 		switch (stream.pos() - startPosition) {
 		case 0:
@@ -1428,7 +1428,7 @@ void Frame::readSpriteD6(Common::MemoryReadStreamEndian &stream, uint16 offset,
 		sprite._width = sprite._height = 0;
 }
 
-void readSpriteDataD6(Common::SeekableReadStreamEndian &stream, Sprite &sprite, uint32 startPosition, uint32 finishPosition) {
+void readSpriteDataD6(Common::SeekableReadStreamEndian &stream, Sprite &sprite, int startPosition, int finishPosition) {
 	while (stream.pos() < finishPosition) {
 		switch (stream.pos() - startPosition) {
 		case 0:
@@ -1879,7 +1879,7 @@ void Frame::readSpriteD7(Common::MemoryReadStreamEndian &stream, uint16 offset,
 		sprite._width = sprite._height = 0;
 }
 
-void readSpriteDataD7(Common::SeekableReadStreamEndian &stream, Sprite &sprite, uint32 startPosition, uint32 finishPosition) {
+void readSpriteDataD7(Common::SeekableReadStreamEndian &stream, Sprite &sprite, int startPosition, int finishPosition) {
 	byte unk[12];
 
 	while (stream.pos() < finishPosition) {
diff --git a/engines/director/frame.h b/engines/director/frame.h
index cb44d81a7c9..eb62e068a79 100644
--- a/engines/director/frame.h
+++ b/engines/director/frame.h
@@ -236,11 +236,11 @@ public:
 	DirectorEngine *_vm;
 };
 
-void readSpriteDataD2(Common::SeekableReadStreamEndian &stream, Sprite &sprite, uint32 startPosition, uint32 finishPosition);
-void readSpriteDataD4(Common::SeekableReadStreamEndian &stream, Sprite &sprite, uint32 startPosition, uint32 finishPosition);
-void readSpriteDataD5(Common::SeekableReadStreamEndian &stream, Sprite &sprite, uint32 startPosition, uint32 finishPosition);
-void readSpriteDataD6(Common::SeekableReadStreamEndian &stream, Sprite &sprite, uint32 startPosition, uint32 finishPosition);
-void readSpriteDataD7(Common::SeekableReadStreamEndian &stream, Sprite &sprite, uint32 startPosition, uint32 finishPosition);
+void readSpriteDataD2(Common::SeekableReadStreamEndian &stream, Sprite &sprite, int startPosition, int finishPosition);
+void readSpriteDataD4(Common::SeekableReadStreamEndian &stream, Sprite &sprite, int startPosition, int finishPosition);
+void readSpriteDataD5(Common::SeekableReadStreamEndian &stream, Sprite &sprite, int startPosition, int finishPosition);
+void readSpriteDataD6(Common::SeekableReadStreamEndian &stream, Sprite &sprite, int startPosition, int finishPosition);
+void readSpriteDataD7(Common::SeekableReadStreamEndian &stream, Sprite &sprite, int startPosition, int finishPosition);
 
 void writeSpriteDataD4(Common::SeekableWriteStream *writeStream, Sprite &sprite);
 void writeSpriteDataD5(Common::SeekableWriteStream *writeStream, Sprite &sprite);




More information about the Scummvm-git-logs mailing list