[Scummvm-git-logs] scummvm master -> 4a6295260428e787f4f6193f314f53945ee00bdc

sev- noreply at scummvm.org
Tue Jul 14 22:46:56 UTC 2026


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

Summary:
bc4c84de44 DIRECTOR: Register Asset Xtra cast members via a lookup table
4a62952604 DIRECTOR: LINGO: Dispatch prepareFrame to frame and movie scripts


Commit: bc4c84de448084e69d4a597f0c2ac39bdcf2b1ef
    https://github.com/scummvm/scummvm/commit/bc4c84de448084e69d4a597f0c2ac39bdcf2b1ef
Author: Gianluca Boiano (morf3089 at gmail.com)
Date: 2026-07-15T00:46:52+02:00

Commit Message:
DIRECTOR: Register Asset Xtra cast members via a lookup table

XtraCastMember parsed the symbol and payload envelope but nothing
consumed them. Registered Asset Xtras now instantiate their own cast
member, parsing their own payload and child resources.

Fixes missing "Text" Asset Xtra rendering in Physicus/Physikus (Intro.dxr)

Changed paths:
  A engines/director/lingo/xtras-cast/textxtra.cpp
  A engines/director/lingo/xtras-cast/textxtra.h
    engines/director/cast.cpp
    engines/director/castmember/digitalvideo.cpp
    engines/director/castmember/digitalvideo.h
    engines/director/castmember/xtra.cpp
    engines/director/castmember/xtra.h
    engines/director/module.mk


diff --git a/engines/director/cast.cpp b/engines/director/cast.cpp
index b499a056526..597cf308c7b 100644
--- a/engines/director/cast.cpp
+++ b/engines/director/cast.cpp
@@ -1657,10 +1657,12 @@ void Cast::loadCastData(Common::SeekableReadStreamEndian &stream, uint16 id, Res
 		debugC(3, kDebugLoading, "Cast::loadCastData(): loading kCastTransition (id=%d, %d children)",  id, res->children.size());
 		target = new TransitionCastMember(this, id, castStream, _version);
 		break;
-	case kCastXtra:
+	case kCastXtra: {
 		debugC(3, kDebugLoading, "Cast::loadCastData(): loading kCastXtra (id=%d, %d children)",  id, res->children.size());
-		target = new XtraCastMember(this, id, castStream, _version);
+		XtraCastMember *xtra = new XtraCastMember(this, id, castStream, _version);
+		target = XtraCastMember::promote(this, id, xtra);
 		break;
+	}
 	default:
 		warning("BUILDBOT: STUB: Cast::loadCastData(): Unhandled cast type: %d [%s] (id=%d, %d children)! This will be missing from the movie and may cause problems", castType, tag2str(castType), id, res->children.size());
 		// also don't try and read the strings... we don't know what this item is.
diff --git a/engines/director/castmember/digitalvideo.cpp b/engines/director/castmember/digitalvideo.cpp
index c6311a75c28..251449b0770 100644
--- a/engines/director/castmember/digitalvideo.cpp
+++ b/engines/director/castmember/digitalvideo.cpp
@@ -39,6 +39,7 @@
 #include "director/movie.h"
 #include "director/window.h"
 #include "director/castmember/digitalvideo.h"
+#include "director/castmember/xtra.h"
 #include "director/lingo/lingo-the.h"
 
 namespace Director {
@@ -211,6 +212,47 @@ DigitalVideoCastMember::~DigitalVideoCastMember() {
 		delete _video;
 }
 
+// QuickTime Asset Xtra property flags, stored as a BE32 word after the
+// 16-bit serialization version in the member's Xtra payload.
+enum {
+	kQTflag_Controller  = 0x1,
+	kQTflag_Crop        = 0x2,
+	kQTflag_Center      = 0x4,
+	kQTflag_Video       = 0x8,
+	kQTflag_DTS         = 0x10,
+	kQTflag_InvertMask  = 0x20,
+	kQTflag_Loop        = 0x40,
+	kQTflag_Preload     = 0x80,
+	kQTflag_PAS         = 0x100,
+	kQTflag_Sound       = 0x200,
+	kQTflag_Mask        = 0x400,
+	kQTflag_Rotation    = 0x800,
+	kQTflag_Translation = 0x1000,
+	kQTflag_Scale       = 0x2000,
+	kQTflag_FrameRate   = 0x4000,
+	kQTflag_AlphaMask   = 0x8000,
+	kQTflag_Stream      = 0x10000
+};
+
+CastMember *DigitalVideoCastMember::createFromXtra(Cast *cast, uint16 castId, XtraCastMember *xtra) {
+	DigitalVideoCastMember *dv = new DigitalVideoCastMember(cast, castId);
+	dv->_qtmovie = true;
+	const Common::Array<byte> &data = xtra->getXtraData();
+	if (data.size() >= 8) {
+		uint32 flags = READ_BE_UINT32(&data[4]);
+		dv->_showControls = (flags & kQTflag_Controller) != 0;
+		dv->_crop = (flags & kQTflag_Crop) != 0;
+		dv->_center = (flags & kQTflag_Center) != 0;
+		dv->_enableVideo = (flags & kQTflag_Video) != 0;
+		dv->_directToStage = (flags & kQTflag_DTS) != 0;
+		dv->_looping = (flags & kQTflag_Loop) != 0;
+		dv->_preload = (flags & kQTflag_Preload) != 0;
+		dv->_pausedAtStart = (flags & kQTflag_PAS) != 0;
+		dv->_enableSound = (flags & kQTflag_Sound) != 0;
+	}
+	return dv;
+}
+
 bool DigitalVideoCastMember::loadVideoFromCast() {
 	Common::String path = getCast()->getVideoPath(_castId);
 	if (!path.empty())
diff --git a/engines/director/castmember/digitalvideo.h b/engines/director/castmember/digitalvideo.h
index aef037dc91f..315cf912918 100644
--- a/engines/director/castmember/digitalvideo.h
+++ b/engines/director/castmember/digitalvideo.h
@@ -30,6 +30,8 @@ class VideoDecoder;
 
 namespace Director {
 
+class XtraCastMember;
+
 enum DigitalVideoType {
 	kDVQuickTime,
 	kDVVideoForWindows,
@@ -45,6 +47,8 @@ public:
 
 	CastMember *duplicate(Cast *cast, uint16 castId) override { return (CastMember *)(new DigitalVideoCastMember(cast, castId, *this)); }
 
+	static CastMember *createFromXtra(Cast *cast, uint16 castId, XtraCastMember *xtra);
+
 	bool isModified() override;
 	Graphics::MacWidget *createWidget(Common::Rect &bbox, Channel *channel, SpriteType spriteType) override;
 
diff --git a/engines/director/castmember/xtra.cpp b/engines/director/castmember/xtra.cpp
index b005af652cd..d78b3078d78 100644
--- a/engines/director/castmember/xtra.cpp
+++ b/engines/director/castmember/xtra.cpp
@@ -19,14 +19,44 @@
  *
  */
 
+#include "common/hash-str.h"
+#include "common/hashmap.h"
+
 #include "director/director.h"
 #include "director/cast.h"
 #include "director/movie.h"
 #include "director/castmember/xtra.h"
+#include "director/castmember/digitalvideo.h"
 #include "director/lingo/lingo-the.h"
+#include "director/lingo/xtras-cast/textxtra.h"
 
 namespace Director {
 
+struct XtraCastMemberProto {
+	const char *symbol;
+	CastMember *(*promote)(Cast *cast, uint16 castId, XtraCastMember *xtra);
+};
+
+static const XtraCastMemberProto xtraCastMemberProtos[] = {
+	{ "quickTimeMedia", DigitalVideoCastMember::createFromXtra },
+	{ "text", TextXtra::createCastMember },
+	{ "font", nullptr },
+	{ nullptr, nullptr },
+};
+
+static const XtraCastMemberProto *findXtraCastMemberProto(const Common::String &symbol) {
+	static Common::HashMap<Common::String, const XtraCastMemberProto *, Common::IgnoreCase_Hash, Common::IgnoreCase_EqualTo> map;
+
+	if (map.empty()) {
+		for (const XtraCastMemberProto *p = xtraCastMemberProtos; p->symbol; p++)
+			map.setVal(p->symbol, p);
+	}
+
+	if (map.contains(symbol))
+		return map.getVal(symbol);
+	return nullptr;
+}
+
 XtraCastMember::XtraCastMember(Cast *cast, uint16 castId, Common::SeekableReadStreamEndian &stream, uint16 version)
 		: CastMember(cast, castId, stream) {
 	_type = kCastXtra;
@@ -42,9 +72,6 @@ XtraCastMember::XtraCastMember(Cast *cast, uint16 castId, Common::SeekableReadSt
 	} else {
 		uint32 symbolLen = stream.readUint32BE();
 		_xtraSymbol = stream.readString(0, symbolLen);
-
-		// TODO: Lookup synmbol
-
 		uint32 xtraDataLen = stream.readUint32BE();
 		xtraDataLen = MIN<int>(xtraDataLen, (int)(stream.size() - stream.pos()));
 		_xtraData = Common::Array<byte>(xtraDataLen);
@@ -55,17 +82,26 @@ XtraCastMember::XtraCastMember(Cast *cast, uint16 castId, Common::SeekableReadSt
 		if (debugChannelSet(5, kDebugLoading)) {
 			Common::hexdump(_xtraData.data(), xtraDataLen);
 		}
-
-		// TODO: Process data in the Xtra
 	}
 
-	warning("STUB: XtraCastMember::XtraCastMember(): Xtra cast members not yet supported for version v%d (%d)", humanVersion(_cast->_version), _cast->_version);
+	if (!findXtraCastMemberProto(_xtraSymbol))
+		warning("STUB: XtraCastMember::XtraCastMember(): Xtra cast members not yet supported for version v%d (%d)", humanVersion(_cast->_version), _cast->_version);
 }
 
 XtraCastMember::XtraCastMember(Cast *cast, uint16 castId, XtraCastMember &source)
 		: CastMember(cast, castId) {
 }
 
+CastMember *XtraCastMember::promote(Cast *cast, uint16 castId, XtraCastMember *xtra) {
+	// Lookup the Xtra and instantiate it if registered
+	const XtraCastMemberProto *p = findXtraCastMemberProto(xtra->_xtraSymbol);
+	if (!p || !p->promote)
+		return xtra;
+	CastMember *promoted = p->promote(cast, castId, xtra);
+	delete xtra;
+	return promoted;
+}
+
 bool XtraCastMember::hasField(int field) {
 	switch (field) {
 	case kTheCuePointNames:		// D6
diff --git a/engines/director/castmember/xtra.h b/engines/director/castmember/xtra.h
index efeaf836638..fea60fe5c14 100644
--- a/engines/director/castmember/xtra.h
+++ b/engines/director/castmember/xtra.h
@@ -33,6 +33,11 @@ public:
 
 	CastMember *duplicate(Cast *cast, uint16 castId) override { return (CastMember *)(new XtraCastMember(cast, castId, *this)); }
 
+	const Common::String &getXtraSymbol() const { return _xtraSymbol; }
+	const Common::Array<byte> &getXtraData() const { return _xtraData; }
+
+	static CastMember *promote(Cast *cast, uint16 castId, XtraCastMember *xtra);
+
 	bool hasField(int field) override;
 	Datum getField(int field) override;
 	void setField(int field, const Datum &value) override;
diff --git a/engines/director/lingo/xtras-cast/textxtra.cpp b/engines/director/lingo/xtras-cast/textxtra.cpp
new file mode 100644
index 00000000000..840cd89e51c
--- /dev/null
+++ b/engines/director/lingo/xtras-cast/textxtra.cpp
@@ -0,0 +1,230 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/endian.h"
+
+#include "graphics/macgui/macfontmanager.h"
+#include "graphics/macgui/mactext.h"
+#include "graphics/macgui/macwindowmanager.h"
+
+#include "director/director.h"
+#include "director/cast.h"
+#include "director/castmember/xtra.h"
+#include "director/lingo/lingo-the.h"
+#include "director/lingo/xtras-cast/textxtra.h"
+#include "director/window.h"
+
+namespace Director {
+
+namespace TextXtra {
+
+static bool isHexDigit(byte c) {
+	return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f');
+}
+
+static uint hexValue(byte c) {
+	if (c >= '0' && c <= '9')
+		return c - '0';
+	if (c >= 'A' && c <= 'F')
+		return c - 'A' + 10;
+	return c - 'a' + 10;
+}
+
+// XMED payload is an ASCII-hex serialized Hermes-Paige document:
+// "FFFF" header, then 0x00 <hex length> ',' <text>, then style runs.
+bool decodeXMED(const Common::Array<byte> &data, Common::String &text) {
+	uint32 size = data.size();
+	if (size < 4 || memcmp(data.data(), "FFFF", 4) != 0)
+		return false;
+
+	for (uint32 i = 0; i + 2 < size; i++) {
+		if (data[i] != 0x00)
+			continue;
+
+		uint32 j = i + 1;
+		uint32 len = 0;
+		int digits = 0;
+		while (j < size && isHexDigit(data[j]) && digits < 6) {
+			len = len * 16 + hexValue(data[j]);
+			j++;
+			digits++;
+		}
+
+		if (digits == 0 || len == 0 || j >= size || data[j] != ',')
+			continue;
+		j++;
+
+		if (j + len > size)
+			continue;
+
+		uint32 printable = 0;
+		for (uint32 k = 0; k < len; k++) {
+			byte c = data[j + k];
+			if ((c >= 0x20 && c <= 0x7e) || c >= 0x80 || c == '\r' || c == '\n' || c == '\t')
+				printable++;
+		}
+
+		if (printable * 5 < len * 4)
+			continue;
+
+		text = Common::String((const char *)&data[j], len);
+		return true;
+	}
+	return false;
+}
+
+// 76-byte "text" payload observed in Physikus (D7): BE32 height at
+// offset 36, width at offset 40, mirroring the authored xtraRect.
+bool parseXtraData(const Common::Array<byte> &data, Common::Rect &rect) {
+	if (data.size() < 44)
+		return false;
+
+	int32 h = READ_BE_INT32(&data[36]);
+	int32 w = READ_BE_INT32(&data[40]);
+	if (w <= 0 || h <= 0 || w > 0x4000 || h > 0x4000)
+		return false;
+
+	rect = Common::Rect((int16)w, (int16)h);
+	return true;
+}
+
+CastMember *createCastMember(Cast *cast, uint16 castId, XtraCastMember *xtra) {
+	return new TextXtraCastMember(cast, castId, *xtra);
+}
+
+} // End of namespace TextXtra
+
+TextXtraCastMember::TextXtraCastMember(Cast *cast, uint16 castId, XtraCastMember &source)
+		: CastMember(cast, castId) {
+	// Not kCastText: the engine casts kCastText members to TextCastMember
+	_type = kCastXtra;
+
+	Common::Rect rect;
+	if (TextXtra::parseXtraData(source.getXtraData(), rect))
+		_initialRect = rect;
+}
+
+TextXtraCastMember::TextXtraCastMember(Cast *cast, uint16 castId, TextXtraCastMember &source)
+		: CastMember(cast, castId) {
+	_type = kCastXtra;
+	_initialRect = source._initialRect;
+	_boundingRect = source._boundingRect;
+	_text = source._text;
+	_loaded = source._loaded;
+	if (cast == source._cast)
+		_children = source._children;
+}
+
+void TextXtraCastMember::load() {
+	if (_loaded)
+		return;
+
+	for (auto &it : _children) {
+		if (it.tag != MKTAG('X', 'M', 'E', 'D'))
+			continue;
+
+		if (!_cast->getArchive()->hasResource(it.tag, it.index)) {
+			warning("TextXtraCastMember::load(): XMED %d not found", it.index);
+			break;
+		}
+		Common::SeekableReadStreamEndian *r = _cast->getArchive()->getResource(it.tag, it.index);
+		Common::Array<byte> data(r->size());
+		r->read(data.data(), r->size());
+		delete r;
+
+		Common::String text;
+		if (TextXtra::decodeXMED(data, text)) {
+			_text = Common::U32String(text, g_director->getPlatformEncoding());
+			debugC(3, kDebugText, "TextXtraCastMember::load(): XMED %d: '%s'", it.index, text.c_str());
+		} else {
+			warning("TextXtraCastMember::load(): XMED %d not decoded", it.index);
+		}
+		break;
+	}
+
+	_loaded = true;
+}
+
+Graphics::MacWidget *TextXtraCastMember::createWidget(Common::Rect &bbox, Channel *channel, SpriteType spriteType) {
+	if (!bbox.width() || !bbox.height())
+		return nullptr;
+
+	// TODO: Parse the authored Paige style runs (font, size, colours)
+	Graphics::MacFont macFont(1, 12);
+	Graphics::MacText *widget = new Graphics::MacText(g_director->getCurrentWindow()->getMacWindow(),
+			bbox.left, bbox.top, bbox.width(), bbox.height(), g_director->_wm, _text, &macFont,
+			g_director->_wm->_colorWhite, g_director->_wm->_colorBlack, _initialRect.width(),
+			Graphics::kTextAlignLeft);
+	widget->draw();
+	return widget;
+}
+
+bool TextXtraCastMember::hasField(int field) {
+	switch (field) {
+	case kTheText:
+		return true;
+	default:
+		break;
+	}
+	return CastMember::hasField(field);
+}
+
+Datum TextXtraCastMember::getField(int field) {
+	Datum d;
+
+	switch (field) {
+	case kTheCastType:
+	case kTheType:
+		d = Common::String("text");
+		d.type = SYMBOL;
+		break;
+	case kTheText:
+		load();
+		d = _text.encode(Common::kUtf8);
+		break;
+	default:
+		d = CastMember::getField(field);
+		break;
+	}
+
+	return d;
+}
+
+void TextXtraCastMember::setField(int field, const Datum &d) {
+	switch (field) {
+	case kTheText:
+		_text = Common::U32String(d.asString(), Common::kUtf8);
+		_loaded = true;
+		_modified = true;
+		return;
+	default:
+		break;
+	}
+
+	CastMember::setField(field, d);
+}
+
+Common::String TextXtraCastMember::formatInfo() {
+	return Common::String::format("initialRect: %dx%d, text: \"%s\"",
+		_initialRect.width(), _initialRect.height(), Common::toPrintable(_text.encode()).c_str());
+}
+
+} // End of namespace Director
diff --git a/engines/director/lingo/xtras-cast/textxtra.h b/engines/director/lingo/xtras-cast/textxtra.h
new file mode 100644
index 00000000000..0f3f3bde582
--- /dev/null
+++ b/engines/director/lingo/xtras-cast/textxtra.h
@@ -0,0 +1,70 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef DIRECTOR_LINGO_XTRAS_CAST_TEXTXTRA_H
+#define DIRECTOR_LINGO_XTRAS_CAST_TEXTXTRA_H
+
+#include "common/array.h"
+#include "common/rect.h"
+#include "common/str.h"
+#include "common/ustr.h"
+
+#include "director/castmember/castmember.h"
+
+namespace Director {
+
+class XtraCastMember;
+
+namespace TextXtra {
+
+CastMember *createCastMember(Cast *cast, uint16 castId, XtraCastMember *xtra);
+bool decodeXMED(const Common::Array<byte> &data, Common::String &text);
+bool parseXtraData(const Common::Array<byte> &data, Common::Rect &rect);
+
+} // End of namespace TextXtra
+
+// "Text" Asset Xtra cast member (D7+), display-only in the Projector.
+// Successor of the RTE rich text pipeline: both serialize Hermes-Paige
+// documents (https://github.com/nmatavka/Hermes-Paige), RTE as binary
+// chunks, this one as an ASCII-hex encoded 'XMED' child resource.
+class TextXtraCastMember : public CastMember {
+public:
+	TextXtraCastMember(Cast *cast, uint16 castId, XtraCastMember &source);
+	TextXtraCastMember(Cast *cast, uint16 castId, TextXtraCastMember &source);
+
+	CastMember *duplicate(Cast *cast, uint16 castId) override { return (CastMember *)(new TextXtraCastMember(cast, castId, *this)); }
+
+	void load() override;
+
+	Graphics::MacWidget *createWidget(Common::Rect &bbox, Channel *channel, SpriteType spriteType) override;
+
+	bool hasField(int field) override;
+	Datum getField(int field) override;
+	void setField(int field, const Datum &value) override;
+
+	Common::String formatInfo() override;
+
+	Common::U32String _text;
+};
+
+} // End of namespace Director
+
+#endif
diff --git a/engines/director/module.mk b/engines/director/module.mk
index 2ae2f4e25c1..817740df20b 100644
--- a/engines/director/module.mk
+++ b/engines/director/module.mk
@@ -188,6 +188,7 @@ MODULE_OBJS = \
 	lingo/xlibs/x/xsoundxfcn.o \
 	lingo/xlibs/x/xwin.o \
 	lingo/xlibs/y/yasix.o \
+	lingo/xtras-cast/textxtra.o \
 	lingo/xtras/a/audio.o \
 	lingo/xtras/b/border.o \
 	lingo/xtras/b/budapi.o \


Commit: 4a6295260428e787f4f6193f314f53945ee00bdc
    https://github.com/scummvm/scummvm/commit/4a6295260428e787f4f6193f314f53945ee00bdc
Author: Gianluca Boiano (morf3089 at gmail.com)
Date: 2026-07-15T00:46:52+02:00

Commit Message:
DIRECTOR: LINGO: Dispatch prepareFrame to frame and movie scripts

When no sprite has behaviors, broadcastEvent() queued nothing, so a
frame or movie script's prepareFrame handler never ran.

Fixes scene navigation (_AktSeite never set) in Physicus/Physikus (Game/GSteuer.cxt, BehaviorScript 7)

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


diff --git a/engines/director/lingo/lingo-events.cpp b/engines/director/lingo/lingo-events.cpp
index 2f2fef8560c..52ffb5e3cbc 100644
--- a/engines/director/lingo/lingo-events.cpp
+++ b/engines/director/lingo/lingo-events.cpp
@@ -690,6 +690,13 @@ void Movie::broadcastEvent(LEvent event) {
 			queueEvent(queue, event, i);
 		}
 	}
+
+	// With no sprite behaviors, cast/frame/movie handlers were never queued
+	// at all; add a no-target pass. When behaviors exist, their pass()
+	// semantics already decide whether those handlers run.
+	if (queue.empty())
+		queueEvent(queue, event, 0);
+
 	_vm->setCurrentWindow(this->getWindow());
 	_lingo->processEvents(queue, false);
 }




More information about the Scummvm-git-logs mailing list