[Scummvm-git-logs] scummvm master -> 217f733e8a90dc44e1e2bd8cf484680c52989211

mduggan noreply at scummvm.org
Mon Dec 16 07:31:21 UTC 2024


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

Summary:
1a374b66aa DGDS: Start to implement Willy Beamish CD version heads
b4f70c660d DGDS: Refactor scene class a bit
217f733e8a DGDS: Fix some issues in train game from Coverity


Commit: 1a374b66aa3d15b7a6320cd22faa48c2c012cd1f
    https://github.com/scummvm/scummvm/commit/1a374b66aa3d15b7a6320cd22faa48c2c012cd1f
Author: Matthew Duggan (mgithub at guarana.org)
Date: 2024-12-16T18:23:29+11:00

Commit Message:
DGDS: Start to implement Willy Beamish CD version heads

Still quite broken but making some progress.

Changed paths:
  A engines/dgds/head.cpp
  A engines/dgds/head.h
    engines/dgds/image.cpp
    engines/dgds/module.mk
    engines/dgds/parser.cpp
    engines/dgds/resource.cpp
    engines/dgds/scene.cpp
    engines/dgds/scene.h
    engines/dgds/sound_raw.cpp
    engines/dgds/sound_raw.h
    engines/dgds/ttm.cpp
    engines/dgds/ttm.h


diff --git a/engines/dgds/head.cpp b/engines/dgds/head.cpp
new file mode 100644
index 00000000000..45e7bea81e2
--- /dev/null
+++ b/engines/dgds/head.cpp
@@ -0,0 +1,217 @@
+/* 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 "dgds/head.h"
+#include "dgds/dgds.h"
+#include "dgds/image.h"
+#include "dgds/sound_raw.h"
+
+namespace Dgds {
+
+void TalkDataHead::drawHead(Graphics::ManagedSurface *dst, const TalkData &data) const {
+	uint drawtype = _drawType ? _drawType : 1;
+	// Use specific head shape if available (eg, in Willy Beamish), if not use talk data shape
+	Common::SharedPtr<Image> img = _shape;
+	if (!img)
+		img = data._shape;
+	if (!img)
+		return;
+	switch (drawtype) {
+	case 1:
+		drawHeadType1(dst, *img);
+		break;
+	case 2:
+		drawHeadType2(dst, *img);
+		break;
+	case 3:
+		if (DgdsEngine::getInstance()->getGameId() == GID_WILLY)
+			drawHeadType3Beamish(dst, data);
+		else
+			drawHeadType3(dst, *img);
+		break;
+	default:
+		error("Unsupported head draw type %d", drawtype);
+	}
+}
+
+void TalkDataHead::drawHeadType1(Graphics::ManagedSurface *dst, const Image &img) const {
+	Common::Rect r = _rect.toCommonRect();
+	dst->fillRect(r, _drawCol);
+	r.grow(-1);
+	dst->fillRect(r, _drawCol == 0 ? 15 : 0);
+	r.left += 2;
+	r.top += 2;
+	const int x = _rect.x;
+	const int y = _rect.y;
+	if (img.isLoaded()) {
+		for (const auto &frame : _headFrames) {
+			img.drawBitmap(frame._frameNo & 0xff, x + frame._xoff, y + frame._yoff, r, *dst);
+		}
+	}
+}
+
+void TalkDataHead::drawHeadType2(Graphics::ManagedSurface *dst, const Image &img) const {
+	if (!img.isLoaded())
+		return;
+	const Common::Rect r = _rect.toCommonRect();
+	for (const auto &frame : _headFrames) {
+		img.drawBitmap(frame._frameNo & 0xff, r.left + frame._xoff, r.top + frame._yoff, r, *dst);
+	}
+}
+
+void TalkDataHead::drawHeadType3Beamish(Graphics::ManagedSurface *dst, const TalkData &data) const {
+	const Common::Rect r = _rect.toCommonRect();
+
+	// Note: only really need the 1px border here but just fill the box.
+	dst->fillRect(r, 8);
+
+	Common::Rect fillRect(r);
+	fillRect.grow(-1);
+	dst->fillRect(fillRect, _drawCol);
+
+	for (const auto &frame : _headFrames) {
+		int frameNo = frame._frameNo & 0x7fff;
+		bool useHeadShape = frame._frameNo & 0x8000;
+
+		Common::SharedPtr<Image> img = useHeadShape ? _shape : data._shape;
+		if (!img || !img->isLoaded() || frameNo >= img->loadedFrameCount())
+			continue;
+
+		ImageFlipMode flip = kImageFlipNone;
+		// Yes, the numerical values are revesed here (1 -> 2 and 2 -> 1).
+		// The head flip flags are reversed from the image draw flags.
+		if (frame._flipFlags & 1)
+			flip = static_cast<ImageFlipMode>(flip & kImageFlipH);
+		if (frame._flipFlags & 2)
+			flip = static_cast<ImageFlipMode>(flip & kImageFlipV);
+
+		img->drawBitmap(frameNo, r.left + frame._xoff, r.top + frame._yoff, fillRect, *dst);
+	}
+}
+
+void TalkDataHead::drawHeadType3(Graphics::ManagedSurface *dst, const Image &img) const {
+	Common::Rect r = _rect.toCommonRect();
+	dst->fillRect(r, 0);
+	if (!img.isLoaded())
+		return;
+	for (const auto &frame : _headFrames) {
+		int frameNo = frame._frameNo;
+		if (frameNo < img.loadedFrameCount())
+			img.drawBitmap(frameNo, r.left + frame._xoff, r.top + frame._yoff, r, *dst);
+		else
+			dst->fillRect(r, 4);
+	}
+}
+
+void TalkDataHead::updateHead() {
+	warning("TODO: Update head");
+	_flags = static_cast<HeadFlags>(_flags & ~(kHeadFlag1 | kHeadFlag8 | kHeadFlag10 | kHeadFlagVisible));
+
+	/* This seems to just be a "needs redraw" flag, but we always redraw
+	for (auto tds : _talkData) {
+		for (auto h : tds._heads) {
+			if ((h._flags & kHeadFlagVisible) && !(h._flags & (kHeadFlag8 | kHeadFlag10 | kHeadFlag80))) {
+				if (h._rect.toCommonRect().intersects(head._rect.toCommonRect())) {
+					h._flags = static_cast<HeadFlags>(h._flags | kHeadFlag4);
+				}
+			}
+		}
+	}
+	*/
+}
+
+
+void TalkData::updateVisibleHeads() {
+	for (auto &head : _heads) {
+		if (head._flags & kHeadFlagVisible)
+			head.updateHead();
+	}
+}
+
+void TalkData::drawVisibleHeads(Graphics::ManagedSurface *dst) const {
+	for (const auto &h : _heads) {
+		if ((h._flags & kHeadFlagVisible) && !(h._flags & kHeadFlag40)) {
+			h.drawHead(dst, *this);
+		}
+	}
+}
+
+bool TalkData::hasVisibleHead() const {
+	for (const auto &h : _heads) {
+		if (h._flags & kHeadFlagVisible)
+			return true;
+	}
+	return false;
+}
+
+//////
+
+void Conversation::unload() {
+	if (_sound) {
+		_sound->stop();
+		_sound.reset();
+	}
+	_img.reset();
+	_ttmScript.reset();
+	_ttmEnv = TTMEnviro();
+}
+
+void Conversation::loadData(uint16 dlgFileNum, uint16 dlgNum, int16 sub) {
+	unload();
+
+	DgdsEngine *engine = DgdsEngine::getInstance();
+	ResourceManager *resourceManager = engine->getResourceManager();
+	Decompressor *decompressor = engine->getDecompressor();
+
+	Common::String fname;
+	if (sub >= 0) {
+		assert(sub < 26);
+		fname = Common::String::format("F%dB%d%c.CDS", dlgFileNum, dlgNum, 'A' + sub);
+	} else {
+		fname = Common::String::format("F%dB%d.CDS", dlgFileNum, dlgNum);
+	}
+
+	_sound.reset(new SoundRaw(resourceManager, decompressor));
+	_sound->load(fname);
+	_img.reset(new Image(resourceManager, decompressor));
+	_img->loadBitmap(fname);
+	_ttmScript.reset(new TTMInterpreter(engine));
+	_ttmScript->load(fname, _ttmEnv);
+	_ttmScript->findAndAddSequences(_ttmEnv, _ttmSeqs);
+
+	// The scripts are desiged so the resources are patchable, but by default
+	// they use the sound and image data from the CDS file.
+	_ttmEnv._soundRaw = _sound;
+	_ttmEnv._scriptShapes[0] = _img;
+	_ttmEnv._cdsTarget = _ttmSeqs[0]->_seqNum;
+}
+
+void Conversation::runScript() {
+	if (!_ttmScript)
+		return;
+	for (auto seq : _ttmSeqs) {
+		if (seq->_seqNum == _ttmEnv._cdsTarget) {
+			_ttmScript->run(_ttmEnv, *seq);
+		}
+	}
+}
+
+} // end namespace Dgds
diff --git a/engines/dgds/head.h b/engines/dgds/head.h
new file mode 100644
index 00000000000..d9dbfe5aecc
--- /dev/null
+++ b/engines/dgds/head.h
@@ -0,0 +1,127 @@
+/* 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 DGDS_HEAD_H
+#define DGDS_HEAD_H
+
+#include "common/str.h"
+#include "common/types.h"
+#include "common/ptr.h"
+#include "common/array.h"
+
+#include "graphics/managed_surface.h"
+
+#include "dgds/dgds_rect.h"
+#include "dgds/ttm.h"
+
+// Classes related to talking heads and conversations.
+
+namespace Dgds {
+
+class Image;
+class SoundRaw;
+
+
+class TalkDataHeadFrame {
+public:
+	TalkDataHeadFrame() : _xoff(0), _yoff(0), _frameNo(0), _flipFlags(0) {}
+	Common::String dump(const Common::String &indent) const;
+
+	uint16 _frameNo;
+	int16 _xoff;
+	int16 _yoff;
+	uint16 _flipFlags;
+};
+
+enum HeadFlags {
+	kHeadFlagNone = 0,
+	kHeadFlag1 = 1,
+	kHeadFlag2 = 2,
+	kHeadFlag4 = 4,
+	kHeadFlag8 = 8,
+	kHeadFlag10 = 0x10,
+	kHeadFlagVisible = 0x20,
+	kHeadFlag40 = 0x40,
+	kHeadFlag80 = 0x80,
+};
+
+class TalkData;
+
+class TalkDataHead {
+public:
+	TalkDataHead() : _num(0), _drawType(0), _drawCol(0), _flags(kHeadFlagNone) {}
+	Common::String dump(const Common::String &indent) const;
+
+	void updateHead();
+
+	void drawHead(Graphics::ManagedSurface *dst, const TalkData &data) const;
+	void drawHeadType1(Graphics::ManagedSurface *dst, const Image &img) const;
+	void drawHeadType2(Graphics::ManagedSurface *dst, const Image &img) const;
+	void drawHeadType3(Graphics::ManagedSurface *dst, const Image &img) const;
+	void drawHeadType3Beamish(Graphics::ManagedSurface *dst, const TalkData &data) const;
+
+	uint16 _num;
+	uint16 _drawType;
+	uint16 _drawCol;
+	DgdsRect _rect;
+	Common::Array<TalkDataHeadFrame> _headFrames;
+	Common::String _bmpFile;
+	HeadFlags _flags;
+	Common::SharedPtr<Image> _shape;
+};
+
+/** TDS talking head data from HOC+ */
+class TalkData {
+public:
+	TalkData() : _num(0), _val(0) {}
+	Common::String dump(const Common::String &indent) const;
+
+	uint16 _num;
+	Common::SharedPtr<Image> _shape;
+	Common::Array<TalkDataHead> _heads;
+	uint16 _val;
+	Common::String _bmpFile;
+	
+	void updateVisibleHeads();
+	void drawVisibleHeads(Graphics::ManagedSurface *dst) const;
+	bool hasVisibleHead() const;
+};
+
+/** CDS data from Willy Beamish talkie */
+class Conversation {
+public:
+	Conversation() {}
+
+	void unload();
+	void runScript();
+	void loadData(uint16 num, uint16 num2, int16 sub);
+
+	Common::SharedPtr<SoundRaw> _sound;
+	Common::SharedPtr<Image> _img;
+	Common::SharedPtr<TTMInterpreter> _ttmScript;
+	Common::Array<Common::SharedPtr<TTMSeq>> _ttmSeqs;
+	TTMEnviro _ttmEnv;
+};
+
+
+} // end namespace Dgds
+
+#endif // DGDS_HEAD_H
diff --git a/engines/dgds/image.cpp b/engines/dgds/image.cpp
index 1773b0e8919..029c19144fe 100644
--- a/engines/dgds/image.cpp
+++ b/engines/dgds/image.cpp
@@ -146,8 +146,8 @@ void Image::loadBitmap(const Common::String &filename) {
 		ex = 0;
 	}
 
-	if (ex != EX_BMP) {
-		warning("Unknown bitmap tag: %d", ex);
+	if (ex != EX_BMP && ex != EX_CDS) {
+		warning("Unknown bitmap extension: %d", ex);
 		delete fileStream;
 		return;
 	}
diff --git a/engines/dgds/module.mk b/engines/dgds/module.mk
index 157f6f51287..b9926eadcec 100644
--- a/engines/dgds/module.mk
+++ b/engines/dgds/module.mk
@@ -13,6 +13,7 @@ MODULE_OBJS := \
 	font.o \
 	game_palettes.o \
 	globals.o \
+	head.o \
 	hoc_intro.o \
 	image.o \
 	inventory.o \
diff --git a/engines/dgds/parser.cpp b/engines/dgds/parser.cpp
index cf29c344276..850f4d6328f 100644
--- a/engines/dgds/parser.cpp
+++ b/engines/dgds/parser.cpp
@@ -117,7 +117,7 @@ bool TTMParser::handleChunk(DgdsChunkReader &chunk, ParserData *data) {
 		scriptData->_frameOffsets.resize(scriptData->_totalFrames + 1, -1);
 		break;
 	default:
-		warning("Unexpected chunk '%s' of size %d found in file '%s'", tag2str(chunk.getId()), chunk.getSize(), _filename.c_str());
+		debug("TTMParser: Unexpected chunk '%s' of size %d found in file '%s'", tag2str(chunk.getId()), chunk.getSize(), _filename.c_str());
 		//chunk._contentStream->skip(chunk._size);
 		break;
 	}
@@ -153,7 +153,7 @@ bool ADSParser::handleChunk(DgdsChunkReader &chunk, ParserData *data) {
 	case ID_VER: // Version - ignore
 		break;
 	default:
-		warning("Unexpected chunk '%s' of size %d found in file '%s'", tag2str(chunk.getId()), chunk.getSize(), _filename.c_str());
+		warning("ADSParser: Unexpected chunk '%s' of size %d found in file '%s'", tag2str(chunk.getId()), chunk.getSize(), _filename.c_str());
 		break;
 	}
 	return false;
diff --git a/engines/dgds/resource.cpp b/engines/dgds/resource.cpp
index 0d5a2f90f1b..22de4099f77 100644
--- a/engines/dgds/resource.cpp
+++ b/engines/dgds/resource.cpp
@@ -176,7 +176,7 @@ bool DgdsChunkReader::isPacked() const {
 		packed = (_id == ID_BIN || _id == ID_VGA);
 		break;
 	case EX_CDS:
-		packed = (_id == ID_TT3);
+		packed = (_id == ID_TT3 || _id == ID_BIN || _id == ID_VGA);
 		break;
 	case EX_GDS:
 	case EX_SDS:
diff --git a/engines/dgds/scene.cpp b/engines/dgds/scene.cpp
index 83bdeb56bdd..d4418e67e85 100644
--- a/engines/dgds/scene.cpp
+++ b/engines/dgds/scene.cpp
@@ -47,6 +47,7 @@
 #include "dgds/dragon_native.h"
 #include "dgds/hoc_intro.h"
 #include "dgds/sound_raw.h"
+#include "dgds/ttm.h"
 
 namespace Dgds {
 
@@ -1064,10 +1065,7 @@ void SDSScene::unload() {
 	_triggers.clear();
 	_talkData.clear();
 	_dynamicRects.clear();
-	if (_dlgSound) {
-		_dlgSound->stop();
-		_dlgSound.reset();
-	}
+	_conversation.unload();
 	_sceneDialogFlags = kDlgFlagNone;
 }
 
@@ -1320,193 +1318,21 @@ void SDSScene::freeTalkData(uint16 num) {
 
 void SDSScene::updateVisibleTalkers() {
 	for (auto &data : _talkData) {
-		for (auto &head : data._heads) {
-			if (head._flags & kHeadFlagVisible)
-				updateHead(head);
-		}
-	}
-}
-
-
-bool SDSScene::loadCDSData(uint16 dlgFileNum, uint16 dlgNum, int16 sub) {
-	if (_dlgSound) {
-		_dlgSound->stop();
-		_dlgSound.reset();
-	}
-
-	Common::String fname;
-	if (sub >= 0) {
-		assert(sub < 26);
-		fname = Common::String::format("F%dB%d%c.CDS", dlgFileNum, dlgNum, 'A' + sub);
-	} else {
-		fname = Common::String::format("F%dB%d.CDS", dlgFileNum, dlgNum);
-	}
-
-	DgdsEngine *engine = DgdsEngine::getInstance();
-	ResourceManager *resourceManager = engine->getResourceManager();
-	Common::SeekableReadStream *cdsFile = resourceManager->getResource(fname);
-	if (!cdsFile)
-		return false;
-
-	DgdsChunkReader chunk(cdsFile);
-	Decompressor *decompressor = engine->getDecompressor();
-
-	bool result = false;
-
-	while (chunk.readNextHeader(EX_CDS, fname)) {
-		if (chunk.isContainer()) {
-			continue;
-		}
-
-		chunk.readContent(decompressor);
-		Common::SeekableReadStream *stream = chunk.getContent();
-
-		//
-		// All CDS files contain TT3 sections with little scripts that load
-		// and play a RAW sound file (eg F1B13.CDS loads CSCR013.RAW), but
-		// they also have RAW sections with the sound data, embedded and the named
-		// RAW files don't exist.
-		//
-		if (chunk.isSection(ID_RAW)) {
-			_dlgSound.reset(new SoundRaw(resourceManager, decompressor));
-			_dlgSound->loadFromStream(stream, chunk.getSize());
-			_dlgSound->play();
-			result = true;
-		}
-	}
-
-	delete cdsFile;
-	return result;
-}
-
-void SDSScene::drawHead(Graphics::ManagedSurface *dst, const TalkData &data, const TalkDataHead &head) {
-	uint drawtype = head._drawType ? head._drawType : 1;
-	// Use specific head shape if available (eg, in Willy Beamish), if not use talk data shape
-	Common::SharedPtr<Image> img = head._shape;
-	if (!img)
-		img = data._shape;
-	if (!img)
-		return;
-	switch (drawtype) {
-	case 1:
-		drawHeadType1(dst, head, *img);
-		break;
-	case 2:
-		drawHeadType2(dst, head, *img);
-		break;
-	case 3:
-		if (DgdsEngine::getInstance()->getGameId() == GID_WILLY)
-			drawHeadType3Beamish(dst, data, head);
-		else
-			drawHeadType3(dst, head, *img);
-		break;
-	default:
-		error("Unsupported head draw type %d", drawtype);
-	}
-}
-
-void SDSScene::drawHeadType1(Graphics::ManagedSurface *dst, const TalkDataHead &head, const Image &img) {
-	Common::Rect r = head._rect.toCommonRect();
-	dst->fillRect(r, head._drawCol);
-	r.grow(-1);
-	dst->fillRect(r, head._drawCol == 0 ? 15 : 0);
-	r.left += 2;
-	r.top += 2;
-	const int x = head._rect.x;
-	const int y = head._rect.y;
-	if (img.isLoaded()) {
-		for (const auto &frame : head._headFrames) {
-			img.drawBitmap(frame._frameNo & 0xff, x + frame._xoff, y + frame._yoff, r, *dst);
-		}
-	}
-}
-
-void SDSScene::drawHeadType2(Graphics::ManagedSurface *dst, const TalkDataHead &head, const Image &img) {
-	if (!img.isLoaded())
-		return;
-	const Common::Rect r = head._rect.toCommonRect();
-	for (const auto &frame : head._headFrames) {
-		img.drawBitmap(frame._frameNo & 0xff, r.left + frame._xoff, r.top + frame._yoff, r, *dst);
+		data.updateVisibleHeads();
 	}
 }
 
-void SDSScene::drawHeadType3Beamish(Graphics::ManagedSurface *dst, const TalkData &data, const TalkDataHead &head) {
-	const Common::Rect r = head._rect.toCommonRect();
-
-	// Note: only really need the 1px border here but just fill the box.
-	dst->fillRect(r, 8);
-
-	Common::Rect fillRect(r);
-	fillRect.grow(-1);
-	dst->fillRect(fillRect, head._drawCol);
-
-	for (const auto &frame : head._headFrames) {
-		int frameNo = frame._frameNo & 0x7fff;
-		bool useHeadShape = frame._frameNo & 0x8000;
-
-		Common::SharedPtr<Image> img = useHeadShape ? head._shape : data._shape;
-		if (!img || !img->isLoaded() || frameNo >= img->loadedFrameCount())
-			continue;
-
-		ImageFlipMode flip = kImageFlipNone;
-		// Yes, the numerical values are revesed here (1 -> 2 and 2 -> 1).
-		// The head flip flags are reversed from the image draw flags.
-		if (frame._flipFlags & 1)
-			flip = static_cast<ImageFlipMode>(flip & kImageFlipH);
-		if (frame._flipFlags & 2)
-			flip = static_cast<ImageFlipMode>(flip & kImageFlipV);
-
-		img->drawBitmap(frameNo, r.left + frame._xoff, r.top + frame._yoff, fillRect, *dst);
-	}
-}
-
-void SDSScene::drawHeadType3(Graphics::ManagedSurface *dst, const TalkDataHead &head, const Image &img) {
-	Common::Rect r = head._rect.toCommonRect();
-	dst->fillRect(r, 0);
-	if (!img.isLoaded())
-		return;
-	for (const auto &frame : head._headFrames) {
-		int frameNo = frame._frameNo;
-		if (frameNo < img.loadedFrameCount())
-			img.drawBitmap(frameNo, r.left + frame._xoff, r.top + frame._yoff, r, *dst);
-		else
-			dst->fillRect(r, 4);
-	}
-}
-
-void SDSScene::updateHead(TalkDataHead &head) {
-	warning("TODO: Update head");
-	head._flags = static_cast<HeadFlags>(head._flags & ~(kHeadFlag1 | kHeadFlag8 | kHeadFlag10 | kHeadFlagVisible));
-
-	/* This seems to just be a "needs redraw" flag, but we always redraw
-	for (auto tds : _talkData) {
-		for (auto h : tds._heads) {
-			if ((h._flags & kHeadFlagVisible) && !(h._flags & (kHeadFlag8 | kHeadFlag10 | kHeadFlag80))) {
-				if (h._rect.toCommonRect().intersects(head._rect.toCommonRect())) {
-					h._flags = static_cast<HeadFlags>(h._flags | kHeadFlag4);
-				}
-			}
-		}
-	}
-	*/
-}
-
 void SDSScene::drawVisibleHeads(Graphics::ManagedSurface *dst) {
 	for (const auto &tds : _talkData) {
-		for (const auto &h : tds._heads) {
-			if ((h._flags & kHeadFlagVisible) && !(h._flags & kHeadFlag40)) {
-				drawHead(dst, tds, h);
-			}
-		}
+		tds.drawVisibleHeads(dst);
 	}
+	_conversation.runScript();
 }
 
 bool SDSScene::hasVisibleHead() const {
 	for (const auto &tds : _talkData) {
-		for (const auto &h : tds._heads) {
-			if (h._flags & kHeadFlagVisible)
-				return true;
-		}
+		if (tds.hasVisibleHead())
+			return true;
 	}
 	return false;
 }
@@ -1587,7 +1413,7 @@ void SDSScene::showDialog(uint16 fileNum, uint16 dlgNum) {
 				loadTalkDataAndSetFlags(dialog._talkDataNum, dialog._talkDataHeadNum);
 			}
 
-			loadCDSData(fileNum, dlgNum, -1);
+			_conversation.loadData(fileNum, dlgNum, -1);
 
 			// hide time gets set the first time it's drawn.
 			if (_dlgWithFlagLo8IsClosing && dialog.hasFlag(kDlgFlagLo8)) {
@@ -1653,7 +1479,7 @@ bool SDSScene::checkDialogActive() {
 					// immediately starts another dialog or changes scene, so the sound
 					// doesn't end up playing.
 					// Need to work out how to correctly delay until the sound finishes?
-					loadCDSData(dlg._fileNum, dlg._num, action->num);
+					_conversation.loadData(dlg._fileNum, dlg._num, action->num);
 
 					// Take a copy of the dialog because the actions might change the scene
 					Dialog dlgCopy = dlg;
@@ -1679,6 +1505,9 @@ bool SDSScene::checkDialogActive() {
 			if (dlg._nextDialogDlgNum) {
 				dlg.setFlag(kDlgFlagHiFinished);
 				showDialog(dlg._nextDialogFileNum, dlg._nextDialogDlgNum);
+			} else {
+				// No next dialog clear CDS data
+				_conversation.unload();
 			}
 		}
 		if (dlg.hasFlag(kDlgFlagVisible)) {
diff --git a/engines/dgds/scene.h b/engines/dgds/scene.h
index 75d7412af17..40f61793845 100644
--- a/engines/dgds/scene.h
+++ b/engines/dgds/scene.h
@@ -27,6 +27,7 @@
 #include "common/serializer.h"
 
 #include "dgds/dialog.h"
+#include "dgds/head.h"
 #include "dgds/dgds_rect.h"
 #include "dgds/minigames/shell_game.h"
 
@@ -36,6 +37,8 @@ class ResourceManager;
 class Decompressor;
 class DgdsFont;
 class SoundRaw;
+class TTMInterpreter;
+class TTMEnviro;
 
 enum SceneCondition {
 	kSceneCondNone = 0,
@@ -262,57 +265,6 @@ private:
 };
 
 
-class TalkDataHeadFrame {
-public:
-	TalkDataHeadFrame() : _xoff(0), _yoff(0), _frameNo(0), _flipFlags(0) {}
-	Common::String dump(const Common::String &indent) const;
-
-	uint16 _frameNo;
-	int16 _xoff;
-	int16 _yoff;
-	uint16 _flipFlags;
-};
-
-enum HeadFlags {
-	kHeadFlagNone = 0,
-	kHeadFlag1 = 1,
-	kHeadFlag2 = 2,
-	kHeadFlag4 = 4,
-	kHeadFlag8 = 8,
-	kHeadFlag10 = 0x10,
-	kHeadFlagVisible = 0x20,
-	kHeadFlag40 = 0x40,
-	kHeadFlag80 = 0x80,
-};
-
-class TalkDataHead {
-public:
-	TalkDataHead() : _num(0), _drawType(0), _drawCol(0), _flags(kHeadFlagNone) {}
-	Common::String dump(const Common::String &indent) const;
-
-	uint16 _num;
-	uint16 _drawType;
-	uint16 _drawCol;
-	DgdsRect _rect;
-	Common::Array<TalkDataHeadFrame> _headFrames;
-	Common::String _bmpFile;
-	HeadFlags _flags;
-	Common::SharedPtr<Image> _shape;
-};
-
-class TalkData {
-public:
-	TalkData() : _num(0), _val(0) {}
-	Common::String dump(const Common::String &indent) const;
-
-	uint16 _num;
-	Common::SharedPtr<Image> _shape;
-	Common::Array<TalkDataHead> _heads;
-	uint16 _val;
-	Common::String _bmpFile;
-};
-
-
 /**
  * A scene is described by an SDS file, which points to the ADS script to load
  * and holds the dialog info.
@@ -482,7 +434,6 @@ public:
 	void loadTalkDataAndSetFlags(uint16 talknum, uint16 headnum);
 	void drawVisibleHeads(Graphics::ManagedSurface *dst);
 	bool hasVisibleHead() const;
-	bool loadCDSData(uint16 num, uint16 num2, int16 sub);
 
 	// dragon-specific scene ops
 	void addAndShowTiredDialog();
@@ -505,12 +456,6 @@ protected:
 private:
 	Dialog *getVisibleDialog();
 	bool readTalkData(Common::SeekableReadStream *s, TalkData &dst);
-	void updateHead(TalkDataHead &head);
-	void drawHead(Graphics::ManagedSurface *dst, const TalkData &data, const TalkDataHead &head);
-	void drawHeadType1(Graphics::ManagedSurface *dst, const TalkDataHead &head, const Image &img);
-	void drawHeadType2(Graphics::ManagedSurface *dst, const TalkDataHead &head, const Image &img);
-	void drawHeadType3(Graphics::ManagedSurface *dst, const TalkDataHead &head, const Image &img);
-	void drawHeadType3Beamish(Graphics::ManagedSurface *dst, const TalkData &data, const TalkDataHead &head);
 
 	int _num;
 	Common::Array<SceneOp> _enterSceneOps;
@@ -531,7 +476,7 @@ private:
 	// From here on is mutable stuff that might need saving
 	Common::Array<Dialog> _dialogs;
 	Common::Array<SceneTrigger> _triggers;
-	Common::SharedPtr<SoundRaw> _dlgSound;
+	Conversation _conversation;
 
 	GameItem *_dragItem;
 	bool _shouldClearDlg;
diff --git a/engines/dgds/sound_raw.cpp b/engines/dgds/sound_raw.cpp
index 937055ba1e5..f02b90c6c9a 100644
--- a/engines/dgds/sound_raw.cpp
+++ b/engines/dgds/sound_raw.cpp
@@ -74,4 +74,12 @@ bool SoundRaw::isPlaying() const {
 	return mixer->isSoundHandleActive(_handle);
 }
 
+uint32 SoundRaw::playedOffset() const {
+	if (!isPlaying())
+		return 0xFFFFFFFF;
+	Audio::Mixer *mixer = DgdsEngine::getInstance()->_mixer;
+	uint32 msecs = mixer->getSoundElapsedTime(_handle);
+	return (msecs * 11025) / 1000;
+}
+
 } // end namespace Dgds
diff --git a/engines/dgds/sound_raw.h b/engines/dgds/sound_raw.h
index 1006595a321..e1e423a4824 100644
--- a/engines/dgds/sound_raw.h
+++ b/engines/dgds/sound_raw.h
@@ -46,6 +46,7 @@ public:
 	void stop();
 	bool isPlaying() const;
 	void loadFromStream(Common::SeekableReadStream *stream, int size);
+	uint32 playedOffset() const;
 
 private:
 	Common::Array<byte> _data;
diff --git a/engines/dgds/ttm.cpp b/engines/dgds/ttm.cpp
index 9bdae9d7852..f6c4d97fe28 100644
--- a/engines/dgds/ttm.cpp
+++ b/engines/dgds/ttm.cpp
@@ -172,6 +172,8 @@ static const char *ttmOpName(uint16 op) {
 	case 0x2400: return "PAL DO BLOCK SWAP";
 	case 0x3000: return "GOSUB";
 	case 0x3100: return "SCROLL";
+	case 0x3200: return "CDS FIND TARGET";
+	case 0x3300: return "CDS GOSUB";
 	case 0x4000: return "SET CLIP WINDOW";
 	case 0x4110: return "FADE OUT";
 	case 0x4120: return "FADE IN";
@@ -251,7 +253,9 @@ static const char *ttmOpName(uint16 op) {
 	case 0xc0f0: return "SONG CONTROLLER??";
 	case 0xc100: return "SAMPLE VOL";
 	case 0xc210: return "LOAD RAW SFX";
-	case 0xc220: return "PLAY RAW SFX ??";
+	case 0xc220: return "PLAY RAW SFX";
+	case 0xc240: return "STOP RAW SFX";
+	case 0xc250: return "SYNC RAW SFX";
 	case 0xcf10: return "SFX MASTER VOL";
 
 	default: return "UNKNOWN!!";
@@ -570,7 +574,7 @@ void TTMInterpreter::doDrawDialogForStrings(const TTMEnviro &env, const TTMSeq &
 }
 
 
-void TTMInterpreter::handleOperation(TTMEnviro &env, TTMSeq &seq, uint16 op, byte count, const int16 *ivals, const Common::String &sval, const Common::Array<Common::Point> &pts) {
+bool TTMInterpreter::handleOperation(TTMEnviro &env, TTMSeq &seq, uint16 op, byte count, const int16 *ivals, const Common::String &sval, const Common::Array<Common::Point> &pts) {
 	switch (op) {
 	case 0x0000: // FINISH:	void
 		break;
@@ -643,11 +647,13 @@ void TTMInterpreter::handleOperation(TTMEnviro &env, TTMSeq &seq, uint16 op, byt
 		break;
 	case 0x0ff0: // REFRESH:	void
 		break;
-	case 0x1020: // SET DELAY:	    i:int   [0..n]
+	case 0x1020: { // SET DELAY:	    i:int   [0..n]
 		// TODO: Probably should do this accounting (as well as timeCut and dialogs)
 		// 		 in game frames, not millis.
-		_vm->adsInterpreter()->setScriptDelay((int)round(ivals[0] * MS_PER_FRAME));
+		int delayMillis = (int)round(ivals[0] * MS_PER_FRAME);
+		_vm->adsInterpreter()->setScriptDelay(delayMillis);
 		break;
+	}
 	case 0x1030: // SET BRUSH:	id:int [-1:n]
 		seq._brushNum = ivals[0];
 		break;
@@ -771,6 +777,19 @@ void TTMInterpreter::handleOperation(TTMEnviro &env, TTMSeq &seq, uint16 op, byt
 		_vm->_compositionBuffer.blitFrom(_vm->getBackgroundBuffer());
 		break;
 	}
+	case 0x3200:
+		env._cdsTarget = findGOTOTarget(env, seq, ivals[0]);
+		break;
+	case 0x3300:
+		if (!env._cdsJumped && env._frameOffsets[env._cdsTarget] != env.scr->pos()) {
+			env._cdsJumped = true;
+			int64 prevPos = env.scr->pos();
+			env.scr->seek(env._frameOffsets[env._cdsTarget]);
+			run(env, seq);
+			env.scr->seek(prevPos);
+			env._cdsJumped = false;
+		}
+		break;
 	case 0x4000: // SET CLIP WINDOW x,y,x2,y2:int	[0..320,0..200]
 		// NOTE: params are xmax/ymax, NOT w/h
 		seq._drawWin = Common::Rect(ivals[0], ivals[1], ivals[2] + 1, ivals[3] + 1);
@@ -1072,9 +1091,14 @@ void TTMInterpreter::handleOperation(TTMEnviro &env, TTMSeq &seq, uint16 op, byt
 	case 0xc210: {  // LOAD RAW SFX filename:str
 		if (seq._executed) // this is a one-shot op
 			break;
-		SoundRaw *snd = new SoundRaw(_vm->getResourceManager(), _vm->getDecompressor());
-		snd->load(sval);
-		env._soundRaw.reset(snd);
+		if (_vm->getResourceManager()->hasResource(sval)) {
+			SoundRaw *snd = new SoundRaw(_vm->getResourceManager(), _vm->getDecompressor());
+			snd->load(sval);
+			env._soundRaw.reset(snd);
+		} else {
+			// This happens in Willy Beamish talkie CDS files.
+			debug("TTM 0xC210: Skip loading RAW %s, not found.", sval.c_str());
+		}
 		break;
 	}
 	case 0xc220: {	// PLAY RAW SFX
@@ -1086,6 +1110,26 @@ void TTMInterpreter::handleOperation(TTMEnviro &env, TTMSeq &seq, uint16 op, byt
 			env._soundRaw->play();
 		break;
 	}
+	case 0xc240: {	// STOP RAW SFX
+		if (env._soundRaw) {
+			env._soundRaw->stop();
+		} else {
+			warning("TODO: Trying to stop raw SFX but nothing loaded");
+		}
+	}
+	case 0xc250: {	// SYNC RAW SFX
+		uint16 hi = (uint16)ivals[1];
+		uint16 lo = (uint16)ivals[0];
+		uint32 offset = ((uint32)hi << 16) + lo;
+		debug("TODO: 0xC250 Sync raw sfx?? offset %d", offset);
+		/*
+		if (env._soundRaw->playedOffset() < offset) {
+			// Not played to this point yet.
+			env.scr->seek(-6);
+			return false;
+		}
+		*/
+	}
 	case 0xf010: { // LOAD SCR:	filename:str
 		if (seq._executed) // this is a one-shot op
 			break;
@@ -1100,8 +1144,13 @@ void TTMInterpreter::handleOperation(TTMEnviro &env, TTMSeq &seq, uint16 op, byt
 		if (seq._executed) // this is a one-shot op
 			break;
 		//debug(1, "0xf020: Load bitmap %s to slot %d for env %d", sval.c_str(), env._enviro, seq._currentBmpId);
-		env._scriptShapes[seq._currentBmpId].reset(new Image(_vm->getResourceManager(), _vm->getDecompressor()));
-		env._scriptShapes[seq._currentBmpId]->loadBitmap(sval);
+		if (_vm->getResourceManager()->hasResource(sval)) {
+			env._scriptShapes[seq._currentBmpId].reset(new Image(_vm->getResourceManager(), _vm->getDecompressor()));
+			env._scriptShapes[seq._currentBmpId]->loadBitmap(sval);
+		} else {
+			// This happens in Willy Beamish talkie CDS files.
+			debug("TTM 0xF020: Skip loading BMP %s, not found.", sval.c_str());
+		}
 		break;
 	case 0xf040: { // LOAD FONT:	filename:str
 		if (seq._executed) // this is a one-shot op
@@ -1162,6 +1211,7 @@ void TTMInterpreter::handleOperation(TTMEnviro &env, TTMSeq &seq, uint16 op, byt
 					ttmOpName(op), sval.c_str());
 		break;
 	}
+	return true;
 }
 
 Common::String TTMInterpreter::readTTMStringVal(Common::SeekableReadStream *scr) {
@@ -1230,7 +1280,9 @@ bool TTMInterpreter::run(TTMEnviro &env, TTMSeq &seq) {
 		}
 		debug(10, " (%s)", ttmOpName(op));
 
-		handleOperation(env, seq, op, count, ivals, sval, pts);
+		bool opResult = handleOperation(env, seq, op, count, ivals, sval, pts);
+		if (!opResult)
+			break;
 	}
 
 	return true;
diff --git a/engines/dgds/ttm.h b/engines/dgds/ttm.h
index ab950dc39ad..cf73d26495a 100644
--- a/engines/dgds/ttm.h
+++ b/engines/dgds/ttm.h
@@ -41,7 +41,7 @@ class TTMEnviro : public ScriptParserData {
 public:
 	TTMEnviro() : _totalFrames(330), _enviro(0), _creditScrollMeasure(0),
 			_creditScrollYOffset(0), _xOff(0), _yOff(0), _xScroll(0), _yScroll(0),
-			ScriptParserData() {
+			_cdsTarget(0), _cdsJumped(false), ScriptParserData() {
 		ARRAYCLEAR(_scriptPals);
 	}
 
@@ -65,6 +65,8 @@ public:
 	int16 _xScroll;
 	int16 _yScroll;
 	Common::SharedPtr<SoundRaw> _soundRaw;
+	int16 _cdsTarget; // The GOTO target to use in the CDS script (Willy Beamish talkie)
+	bool _cdsJumped;
 };
 
 enum TTMRunType {
@@ -126,7 +128,7 @@ public:
 	static Common::String readTTMStringVal(Common::SeekableReadStream *scr);
 
 protected:
-	void handleOperation(TTMEnviro &env, TTMSeq &seq, uint16 op, byte count, const int16 *ivals, const Common::String &sval, const Common::Array<Common::Point> &pts);
+	bool handleOperation(TTMEnviro &env, TTMSeq &seq, uint16 op, byte count, const int16 *ivals, const Common::String &sval, const Common::Array<Common::Point> &pts);
 	int32 findGOTOTarget(const TTMEnviro &env, const TTMSeq &seq, int16 frame);
 	void doWipeOp(uint16 code, const TTMEnviro &env, const TTMSeq &seq, const Common::Rect &r);
 	int16 doOpInitCreditScroll(const Image *img);


Commit: b4f70c660d30394b11322a6610bdc140200611cd
    https://github.com/scummvm/scummvm/commit/b4f70c660d30394b11322a6610bdc140200611cd
Author: Matthew Duggan (mgithub at guarana.org)
Date: 2024-12-16T18:27:13+11:00

Commit Message:
DGDS: Refactor scene class a bit

Extract SceneOp and SceneCondition to their own files to reduce file size a
bit.

Also fix some small issues in CDS loading.

Changed paths:
  A engines/dgds/debug_util.h
  A engines/dgds/scene_condition.cpp
  A engines/dgds/scene_condition.h
  A engines/dgds/scene_op.cpp
  A engines/dgds/scene_op.h
    engines/dgds/dialog.cpp
    engines/dgds/head.cpp
    engines/dgds/head.h
    engines/dgds/module.mk
    engines/dgds/scene.cpp
    engines/dgds/scene.h
    engines/dgds/ttm.cpp
    engines/dgds/ttm.h


diff --git a/engines/dgds/debug_util.h b/engines/dgds/debug_util.h
new file mode 100644
index 00000000000..668bf6d1fc8
--- /dev/null
+++ b/engines/dgds/debug_util.h
@@ -0,0 +1,48 @@
+/* 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 DGDS_DEBUG_UTIL_H
+#define DGDS_DEBUG_UTIL_H
+
+#include "common/str.h"
+
+namespace Dgds {
+
+namespace DebugUtil {
+
+template<class C> static Common::String dumpStructList(const Common::String &indent, const Common::String &name, const C &list) {
+	if (list.empty())
+		return "";
+
+	const Common::String nextind = indent + "    ";
+	Common::String str = Common::String::format("\n%s%s=", Common::String(indent + "  ").c_str(), name.c_str());
+	for (const auto &s : list) {
+		str += "\n";
+		str += s.dump(nextind);
+	}
+	return str;
+}
+
+};
+
+} // end namespace Dgds
+
+#endif // DGDS_DEBUG_UTIL_H
diff --git a/engines/dgds/dialog.cpp b/engines/dgds/dialog.cpp
index 8b4607c0d00..b88af143da3 100644
--- a/engines/dgds/dialog.cpp
+++ b/engines/dgds/dialog.cpp
@@ -36,23 +36,10 @@
 #include "dgds/scene.h"
 #include "dgds/font.h"
 #include "dgds/drawing.h"
+#include "dgds/debug_util.h"
 
 namespace Dgds {
 
-// TODO: This is repeated here and in scene.cpp
-template<class S> static Common::String _dumpStructList(const Common::String &indent, const Common::String &name, const Common::Array<S> &list) {
-	if (list.empty())
-		return "";
-
-	const Common::String nextind = indent + "    ";
-	Common::String str = Common::String::format("\n%s%s=", Common::String(indent + "  ").c_str(), name.c_str());
-	for (const auto &s : list) {
-		str += "\n";
-		str += s.dump(nextind);
-	}
-	return str;
-}
-
 
 int Dialog::_lastSelectedDialogItemNum = 0;
 Dialog *Dialog::_lastDialogSelectionChangedFor = nullptr;
@@ -694,7 +681,7 @@ Common::String Dialog::dump(const Common::String &indent) const {
 			_flags, _frameType, _time, _nextDialogFileNum, _nextDialogDlgNum, _talkDataNum, _talkDataHeadNum);
 	str += indent + "state=" + (_state ? _state->dump("") : "null");
 	str += "\n";
-	str += _dumpStructList(indent, "actions", _action);
+	str += DebugUtil::dumpStructList(indent, "actions", _action);
 	str += "\n";
 	str += indent + "  str='" + _str + "'>";
 	return str;
@@ -739,7 +726,7 @@ Common::Error DialogState::syncState(Common::Serializer &s) {
 
 Common::String DialogAction::dump(const Common::String &indent) const {
 	Common::String str = Common::String::format("%sDialogueAction<span: %d-%d", indent.c_str(), strStart, strEnd);
-	str += _dumpStructList(indent, "opList", sceneOpList);
+	str += DebugUtil::dumpStructList(indent, "opList", sceneOpList);
 	if (!sceneOpList.empty()) {
 		str += "\n";
 		str += indent;
diff --git a/engines/dgds/head.cpp b/engines/dgds/head.cpp
index 45e7bea81e2..96c53897b29 100644
--- a/engines/dgds/head.cpp
+++ b/engines/dgds/head.cpp
@@ -178,6 +178,11 @@ void Conversation::loadData(uint16 dlgFileNum, uint16 dlgNum, int16 sub) {
 	unload();
 
 	DgdsEngine *engine = DgdsEngine::getInstance();
+
+	// These files are only present in Willy Beamish CD version
+	if (engine->getGameId() != GID_WILLY)
+		return;
+
 	ResourceManager *resourceManager = engine->getResourceManager();
 	Decompressor *decompressor = engine->getDecompressor();
 
@@ -189,6 +194,9 @@ void Conversation::loadData(uint16 dlgFileNum, uint16 dlgNum, int16 sub) {
 		fname = Common::String::format("F%dB%d.CDS", dlgFileNum, dlgNum);
 	}
 
+	if (!resourceManager->hasResource(fname))
+		return;
+
 	_sound.reset(new SoundRaw(resourceManager, decompressor));
 	_sound->load(fname);
 	_img.reset(new Image(resourceManager, decompressor));
@@ -201,15 +209,37 @@ void Conversation::loadData(uint16 dlgFileNum, uint16 dlgNum, int16 sub) {
 	// they use the sound and image data from the CDS file.
 	_ttmEnv._soundRaw = _sound;
 	_ttmEnv._scriptShapes[0] = _img;
-	_ttmEnv._cdsTarget = _ttmSeqs[0]->_seqNum;
+
+	// Always run seq 1 on init.
+	_ttmEnv._cdsSeqNum = 1;
+	runScript();
 }
 
 void Conversation::runScript() {
 	if (!_ttmScript)
 		return;
+
+	DgdsEngine *engine = DgdsEngine::getInstance();
+	if (_nextExec && engine->getThisFrameMs() < _nextExec)
+		return;
+
+	_nextExec = 0;
+	_ttmEnv._xOff = _drawRect.x;
+	_ttmEnv._yOff = _drawRect.y;
+
 	for (auto seq : _ttmSeqs) {
-		if (seq->_seqNum == _ttmEnv._cdsTarget) {
+		if (seq->_seqNum == _ttmEnv._cdsSeqNum) {
+			debug(10, "CDS: Running TTM sequence %d frame %d", seq->_seqNum, seq->_currentFrame);
+			_ttmEnv.scr->seek(_ttmEnv._frameOffsets[seq->_currentFrame]);
+
+			seq->_drawWin = _drawRect.toCommonRect();
 			_ttmScript->run(_ttmEnv, *seq);
+			if (_ttmEnv._cdsDelay) {
+				_nextExec = engine->getThisFrameMs() + _ttmEnv._cdsDelay;
+				_ttmEnv._cdsDelay = 0;
+			} else {
+				seq->_currentFrame++;
+			}
 		}
 	}
 }
diff --git a/engines/dgds/head.h b/engines/dgds/head.h
index d9dbfe5aecc..b2c7558f5a5 100644
--- a/engines/dgds/head.h
+++ b/engines/dgds/head.h
@@ -99,7 +99,7 @@ public:
 	Common::Array<TalkDataHead> _heads;
 	uint16 _val;
 	Common::String _bmpFile;
-	
+
 	void updateVisibleHeads();
 	void drawVisibleHeads(Graphics::ManagedSurface *dst) const;
 	bool hasVisibleHead() const;
@@ -108,7 +108,7 @@ public:
 /** CDS data from Willy Beamish talkie */
 class Conversation {
 public:
-	Conversation() {}
+	Conversation() : _nextExec(0) {}
 
 	void unload();
 	void runScript();
@@ -119,6 +119,8 @@ public:
 	Common::SharedPtr<TTMInterpreter> _ttmScript;
 	Common::Array<Common::SharedPtr<TTMSeq>> _ttmSeqs;
 	TTMEnviro _ttmEnv;
+	uint32 _nextExec;
+	DgdsRect _drawRect;
 };
 
 
diff --git a/engines/dgds/module.mk b/engines/dgds/module.mk
index b9926eadcec..2dc84cdb616 100644
--- a/engines/dgds/module.mk
+++ b/engines/dgds/module.mk
@@ -23,6 +23,8 @@ MODULE_OBJS := \
 	request.o \
 	resource.o \
 	scene.o \
+	scene_condition.o \
+	scene_op.o \
 	scripts.o \
 	sound.o \
 	sound_raw.o \
diff --git a/engines/dgds/scene.cpp b/engines/dgds/scene.cpp
index d4418e67e85..45a7ca23425 100644
--- a/engines/dgds/scene.cpp
+++ b/engines/dgds/scene.cpp
@@ -26,195 +26,34 @@
 #include "common/system.h"
 #include "common/util.h"
 
-#include "graphics/cursorman.h"
 #include "graphics/surface.h"
-#include "graphics/primitives.h"
 
 #include "dgds/dgds.h"
 #include "dgds/includes.h"
 #include "dgds/resource.h"
-#include "dgds/request.h"
 #include "dgds/scene.h"
 #include "dgds/ads.h"
 #include "dgds/menu.h"
-#include "dgds/font.h"
 #include "dgds/globals.h"
-#include "dgds/image.h"
 #include "dgds/inventory.h"
-#include "dgds/minigames/china_tank.h"
-#include "dgds/minigames/china_train.h"
-#include "dgds/minigames/dragon_arcade.h"
-#include "dgds/dragon_native.h"
-#include "dgds/hoc_intro.h"
-#include "dgds/sound_raw.h"
-#include "dgds/ttm.h"
+#include "dgds/debug_util.h"
 
 namespace Dgds {
 
-template<class C> static Common::String _dumpStructList(const Common::String &indent, const Common::String &name, const C &list) {
-	if (list.empty())
-		return "";
-
-	const Common::String nextind = indent + "    ";
-	Common::String str = Common::String::format("\n%s%s=", Common::String(indent + "  ").c_str(), name.c_str());
-	for (const auto &s : list) {
-		str += "\n";
-		str += s.dump(nextind);
-	}
-	return str;
-}
-
-
-Common::String _sceneConditionStr(SceneCondition cflag) {
-	Common::String ret;
-
-	if (cflag & kSceneCondOr)
-		return "or";
-
-	if (cflag & kSceneCondSceneState)
-		ret += "state|";
-	if (cflag & kSceneCondNeedItemSceneNum)
-		ret += "itemsnum|";
-	if (cflag & kSceneCondNeedItemQuality)
-		ret += "quality|";
-	if ((cflag & (kSceneCondSceneState | kSceneCondNeedItemSceneNum | kSceneCondNeedItemQuality)) == 0)
-		ret += "global|";
-
-	cflag = static_cast<SceneCondition>(cflag & ~(kSceneCondSceneState | kSceneCondNeedItemSceneNum | kSceneCondNeedItemQuality));
-	if (cflag == kSceneCondNone)
-		ret += "nocond";
-	if (cflag & kSceneCondLessThan)
-		ret += "less";
-	if (cflag & kSceneCondEqual)
-		ret += "equal";
-	if (cflag & kSceneCondNegate)
-		ret += "-not";
-	if (cflag & kSceneCondAbsVal)
-		ret += "(abs)";
-
-	return ret;
-}
-
-Common::String SceneConditions::dump(const Common::String &indent) const {
-	return Common::String::format("%sSceneCondition<flg 0x%02x(%s) num %d val %d>", indent.c_str(),
-			_flags, _sceneConditionStr(_flags).c_str(), _num, _val);
-}
-
 
 Common::String HotArea::dump(const Common::String &indent) const {
 	Common::String str = Common::String::format("%sHotArea<%s num %d cursor %d cursor2 %d interactionRectNum %d",
 			indent.c_str(), _rect.dump("").c_str(), _num, _cursorNum, _cursorNum2, _objInteractionRectNum);
-	str += _dumpStructList(indent, "enableConditions", enableConditions);
-	str += _dumpStructList(indent, "onRClickOps", onRClickOps);
-	str += _dumpStructList(indent, "onLDownOps", onLDownOps);
-	str += _dumpStructList(indent, "onLClickOps", onLClickOps);
+	str += DebugUtil::dumpStructList(indent, "enableConditions", enableConditions);
+	str += DebugUtil::dumpStructList(indent, "onRClickOps", onRClickOps);
+	str += DebugUtil::dumpStructList(indent, "onLDownOps", onLDownOps);
+	str += DebugUtil::dumpStructList(indent, "onLClickOps", onLClickOps);
 	str += "\n";
 	str += indent + ">";
 	return str;
 }
 
 
-static Common::String _sceneOpCodeName(SceneOpCode code) {
-	switch (code) {
-	case kSceneOpNone: 		  	return "none";
-	case kSceneOpChangeScene: 	return "changeScene";
-	case kSceneOpNoop:		  	return "noop";
-	case kSceneOpGlobal:		return "global";
-	case kSceneOpSegmentStateOps: return "sceneOpSegmentStateOps";
-	case kSceneOpSetItemAttr:   return "setItemAttr";
-	case kSceneOpSetDragItem:   return "setDragItem";
-	case kSceneOpOpenInventory: return "openInventory";
-	case kSceneOpShowDlg:		return "showdlg";
-	case kSceneOpShowInvButton:	return "showInvButton";
-	case kSceneOpHideInvButton:	return "hideInvButton";
-	case kSceneOpEnableTrigger: return "enabletrigger";
-	case kSceneOpChangeSceneToStored: 	return "changeSceneToStored";
-	case kSceneOpAddFlagToDragItem:		return "addFlagToDragItem";
-	case kSceneOpMoveItemsBetweenScenes: return "moveItemsBetweenScenes";
-	case kSceneOpOpenInventoryZoom:   	return "openInventoryZoom";
-	case kSceneOpShowClock:		return "sceneOpShowClock";
-	case kSceneOpHideClock:		return "sceneOpHideClock";
-	case kSceneOpShowMouse:		return "sceneOpShowMouse";
-	case kSceneOpHideMouse:		return "sceneOpHideMouse";
-	case kSceneOpLoadTalkDataAndSetFlags: return "sceneOpLoadTalkDataAndSetFlags";
-	case kSceneOpDrawVisibleTalkHeads: return "sceneOpDrawVisibleTalksHeads";
-	case kSceneOpLoadTalkData: 	return "sceneOpLoadTalkData";
-	case kSceneOpLoadDDSData: 	return "sceneOpLoadDDSData";
-	case kSceneOpFreeDDSData: 	return "sceneOpFreeDDSData";
-	case kSceneOpFreeTalkData: 	return "sceneOpFreeTalkData";
-
-	default:
-		break;
-	}
-
-	if (DgdsEngine::getInstance()->getGameId() == GID_DRAGON) {
-		switch (code) {
-		case kSceneOpPasscode:		return "passcode";
-		case kSceneOpMeanwhile:   	return "meanwhile";
-		case kSceneOpOpenGameOverMenu: return "openGameOverMenu";
-		case kSceneOpTiredDialog:	return "openTiredDialog";
-		case kSceneOpArcadeTick: 	return "sceneOpArcadeTick";
-		case kSceneOpDrawDragonCountdown1: 	return "drawDragonCountdown1";
-		case kSceneOpDrawDragonCountdown2:	return "drawDragonCountdown2";
-		case kSceneOpOpenPlaySkipIntroMenu: return "openPlaySkipIntroMovie";
-		case kSceneOpOpenBetterSaveGameMenu: return "openBetterSaveGameMenu";
-		default:
-			break;
-		}
-	} else if (DgdsEngine::getInstance()->getGameId() == GID_HOC) {
-		switch (code) {
-		case kSceneOpChinaTankInit:			return "tankInit";
-		case kSceneOpChinaTankEnd:			return "tankEnd";
-		case kSceneOpChinaTankTick:			return "tankTick";
-		case kSceneOpChinaScrollLeft:		return "scrollLeft";
-		case kSceneOpChinaScrollRight:		return "scrollRight";
-		case kSceneOpShellGameInit:			return "shellGameInit";
-		case kSceneOpShellGameEnd:			return "shellGameEnd";
-		case kSceneOpShellGameTick:			return "shellGameTick";
-		case kSceneOpChinaTrainInit:		return "trainInit";
-		case kSceneOpChinaTrainEnd:			return "trainEnd";
-		case kSceneOpChinaTrainTick:		return "trainTick";
-		case kSceneOpChinaOpenGameOverMenu: return "gameOverMenu";
-		case kSceneOpChinaOpenSkipCreditsMenu: return "skipCreditsMenu";
-		case kSceneOpChinaOnIntroInit:		return "chinaOnIntroInit";
-		case kSceneOpChinaOnIntroTick:		return "chinaOnIntroTick";
-		case kSceneOpChinaOnIntroEnd:  		return "chinaOnIntroEnd";
-		default:
-			break;
-		}
-	} else if (DgdsEngine::getInstance()->getGameId() == GID_WILLY) {
-		switch (code) {
-		case kSceneOpOpenBeamishGameOverMenu: return "openGameOverMenu";
-		case kSceneOpOpenBeamishOpenSkipCreditsMenu: return "skipCreditsMenu";
-		default:
-			break;
-		}
-	}
-
-	return Common::String::format("sceneOp%d", (int)code);
-}
-
-Common::String SceneOp::dump(const Common::String &indent) const {
-	Common::String argsStr;
-	if (_args.empty()) {
-		argsStr = "[]";
-	} else {
-		argsStr = "[";
-		for  (uint i : _args)
-			argsStr += Common::String::format("%d ", i);
-		argsStr.setChar(']', argsStr.size() - 1);
-	}
-	Common::String str = Common::String::format("%sSceneOp<op: %s args: %s", indent.c_str(), _sceneOpCodeName(_opCode).c_str(), argsStr.c_str());
-
-	str += _dumpStructList(indent, "conditionList", _conditionList);
-	if (!_conditionList.empty()) {
-		str += "\n";
-		str += indent;
-	}
-	str += ">";
-	return str;
-}
-
 Common::String GameItem::dump(const Common::String &indent) const {
 	Common::String super = HotArea::dump(indent + "  ");
 
@@ -222,8 +61,8 @@ Common::String GameItem::dump(const Common::String &indent) const {
 			"%sGameItem<\n%s\n%saltCursor %d icon %d sceneNum %d flags %d quality %d",
 			indent.c_str(), super.c_str(), indent.c_str(), _altCursor,
 			_iconNum, _inSceneNum, _flags, _quality);
-	str += _dumpStructList(indent, "onDragFinishedOps", onDragFinishedOps);
-	str += _dumpStructList(indent, "onBothButtonsOps", onBothButtonsOps);
+	str += DebugUtil::dumpStructList(indent, "onDragFinishedOps", onDragFinishedOps);
+	str += DebugUtil::dumpStructList(indent, "onBothButtonsOps", onBothButtonsOps);
 	str += "\n";
 	str += indent + ">";
 	return str;
@@ -238,7 +77,7 @@ Common::String MouseCursor::dump(const Common::String &indent) const {
 Common::String ObjectInteraction::dump(const Common::String &indent) const {
 	Common::String str = Common::String::format("%sObjectInteraction<dropped %d target %d", indent.c_str(), _droppedItemNum, _targetItemNum);
 
-	str += _dumpStructList(indent, "opList", opList);
+	str += DebugUtil::dumpStructList(indent, "opList", opList);
 	str += "\n";
 	str += indent + ">";
 	return str;
@@ -247,8 +86,8 @@ Common::String ObjectInteraction::dump(const Common::String &indent) const {
 
 Common::String SceneTrigger::dump(const Common::String &indent) const {
 	Common::String str = Common::String::format("%sSceneTrigger<num %d %s %d", indent.c_str(), _num, _enabled ? "enabled" : "disabled", _timesToCheckBeforeRunning);
-	str += _dumpStructList(indent, "conditionList", conditionList);
-	str += _dumpStructList(indent, "opList", sceneOpList);
+	str += DebugUtil::dumpStructList(indent, "conditionList", conditionList);
+	str += DebugUtil::dumpStructList(indent, "opList", sceneOpList);
 	str += "\n";
 	str += indent + ">";
 	return str;
@@ -623,244 +462,6 @@ void Scene::segmentStateOps(const Common::Array<uint16> &args) {
 }
 
 
-bool Scene::runSceneOp(const SceneOp &op) {
-	DgdsEngine *engine = DgdsEngine::getInstance();
-	switch (op._opCode) {
-	case kSceneOpChangeScene:
-		if (engine->changeScene(op._args[0]))
-			return true;
-		break;
-	case kSceneOpNoop:
-		break;
-	case kSceneOpGlobal:
-		// The globals are held by the GDS scene
-		engine->getGDSScene()->globalOps(op._args);
-		break;
-	case kSceneOpSegmentStateOps:
-		SDSScene::segmentStateOps(op._args);
-		break;
-	case kSceneOpSetItemAttr:
-		SDSScene::setItemAttrOp(op._args);
-		break;
-	case kSceneOpSetDragItem:
-		SDSScene::setDragItemOp(op._args);
-		break;
-	case kSceneOpOpenInventory:
-		engine->getInventory()->open();
-		// This implicitly changes scene num
-		break;
-	case kSceneOpShowDlg:
-		if (op._args.size() == 1)
-			engine->getScene()->showDialog(0, op._args[0]);
-		else if (op._args.size() > 1)
-			engine->getScene()->showDialog(op._args[0], op._args[1]);
-		break;
-	case kSceneOpShowInvButton:
-		engine->getScene()->addInvButtonToHotAreaList();
-		break;
-	case kSceneOpHideInvButton:
-		engine->getScene()->removeInvButtonFromHotAreaList();
-		break;
-	case kSceneOpEnableTrigger:
-		engine->getScene()->enableTrigger(op._args[0]);
-		break;
-	case kSceneOpChangeSceneToStored: {
-		int16 sceneNo = engine->getGameGlobals()->getGlobal(0x61);
-		if (engine->changeScene(sceneNo))
-			return true;
-		break;
-	}
-	case kSceneOpAddFlagToDragItem: {
-		GameItem *item = engine->getScene()->getDragItem();
-		if (item) {
-			item->_flags |= 1;
-			// TODO: Use hot x/y or just position?
-			Common::Point lastMouse = engine->getLastMouseMinusHot();
-			item->_rect.x = lastMouse.x;
-			item->_rect.y = lastMouse.y;
-		}
-		break;
-	}
-	case kSceneOpOpenInventoryZoom:
-		engine->getInventory()->setShowZoomBox(true);
-		engine->getInventory()->open();
-		return true;
-	case kSceneOpMoveItemsBetweenScenes: {
-		int16 fromScene = engine->getGameGlobals()->getGlobal(0x55);
-		int16 toScene = engine->getGameGlobals()->getGlobal(0x54);
-		for (auto &item : engine->getGDSScene()->getGameItems()) {
-			if (item._inSceneNum == fromScene)
-				item._inSceneNum = toScene;
-		}
-		break;
-	}
-	case kSceneOpShowClock:
-		engine->setShowClock(true);
-		break;
-	case kSceneOpHideClock:
-		engine->setShowClock(false);
-		break;
-	case kSceneOpShowMouse:
-		CursorMan.showMouse(true);
-		break;
-	case kSceneOpHideMouse:
-		CursorMan.showMouse(false);
-		break;
-	case kSceneOpLoadTalkDataAndSetFlags: // args: tdsnum to load, headnum
-		engine->getScene()->loadTalkDataAndSetFlags(op._args[0], op._args[1]);
-		break;
-	case kSceneOpDrawVisibleTalkHeads: // args: none
-		engine->getScene()->updateVisibleTalkers();
-		break;
-	case kSceneOpLoadTalkData: 	// args: tds num to load
-		engine->getScene()->loadTalkData(op._args[0]);
-		break;
-	case kSceneOpLoadDDSData: 	// args: dds num to load
-		if (op._args[0])
-			engine->getScene()->loadDialogData(op._args[0]);
-		break;
-	case kSceneOpFreeDDSData:	// args: dds num to free
-		engine->getScene()->freeDialogData(op._args[0]);
-		break;
-	case kSceneOpFreeTalkData: 	// args: tds num to free
-		engine->getScene()->freeTalkData(op._args[0]);
-		break;
-
-	default:
-		warning("TODO: Implement generic scene op %d", op._opCode);
-		break;
-	}
-	return false;
-}
-
-/*static*/
-bool Scene::runDragonOp(const SceneOp &op) {
-	DgdsEngine *engine = DgdsEngine::getInstance();
-	switch (op._opCode) {
-	case kSceneOpPasscode:
-		DragonNative::updatePasscodeGlobal();
-		break;
-	case kSceneOpMeanwhile:
-		// TODO: Should we draw "meanwhile" like the original? it just gets overwritten with the image anyway.
-		// Probably need to do something here to avoid flashing..
-		//engine->_compositionBuffer.fillRect(Common::Rect(SCREEN_WIDTH, SCREEN_HEIGHT), 0);
-		break;
-	case kSceneOpOpenGameOverMenu:
-		engine->setMenuToTrigger(kMenuGameOver);
-		break;
-	case kSceneOpTiredDialog:
-		engine->getInventory()->close();
-		engine->getScene()->addAndShowTiredDialog();
-		break;
-	case kSceneOpArcadeTick:
-		// TODO: Add a configuration option to skip arcade sequence?
-		// g_system->displayMessageOnOSD(_("Skipping DGDS arcade sequence"));
-		// engine->getGameGlobals()->setGlobal(0x21, 6);
-		engine->getDragonArcade()->arcadeTick();
-		break;
-	case kSceneOpDrawDragonCountdown1:
-		DragonNative::drawCountdown(FontManager::k4x5Font, 141, 56);
-		break;
-	case kSceneOpDrawDragonCountdown2:
-		DragonNative::drawCountdown(FontManager::k8x8Font, 250, 42);
-		break;
-	case kSceneOpOpenPlaySkipIntroMenu:
-		engine->setMenuToTrigger(kMenuSkipPlayIntro);
-		break;
-	case kSceneOpOpenBetterSaveGameMenu:
-		engine->setMenuToTrigger(kMenuSaveBeforeArcade);
-		break;
-	default:
-		error("Unexpected Dragon scene opcode %d", op._opCode);
-		break;
-	}
-	return false;
-}
-
-/*static*/
-bool Scene::runChinaOp(const SceneOp &op) {
-	DgdsEngine *engine = DgdsEngine::getInstance();
-	switch (op._opCode) {
-	case kSceneOpChinaTankInit:
-		engine->getChinaTank()->init();
-		break;
-	case kSceneOpChinaTankEnd:
-		engine->getChinaTank()->end();
-		break;
-	case kSceneOpChinaTankTick:
-		engine->getChinaTank()->tick();
-		break;
-	case kSceneOpShellGameTick:
-		engine->getShellGame()->shellGameTick();
-		break;
-	case kSceneOpShellGameEnd:
-		engine->getShellGame()->shellGameEnd();
-		break;
-	case kSceneOpChinaTrainInit:
-		engine->getChinaTrain()->init();
-		break;
-	case kSceneOpChinaTrainEnd:
-		engine->getChinaTrain()->end();
-		break;
-	case kSceneOpChinaTrainTick:
-		engine->getChinaTrain()->tick();
-		break;
-	case kSceneOpChinaOpenGameOverMenu:
-		engine->setMenuToTrigger(kMenuGameOver);
-		break;
-	case kSceneOpChinaOpenSkipCreditsMenu:
-		engine->setMenuToTrigger(kMenuSkipPlayIntro);
-		break;
-	case kSceneOpChinaOnIntroInit:
-		engine->getHocIntro()->init();
-		break;
-	case kSceneOpChinaOnIntroTick:
-		engine->getHocIntro()->tick();
-		break;
-	case kSceneOpChinaOnIntroEnd:
-		engine->getHocIntro()->end();
-		break;
-	case kSceneOpChinaScrollIntro:
-	case kSceneOpChinaScrollLeft:
-	case kSceneOpChinaScrollRight:
-		// These map to null functions.
-		break;
-	default:
-		warning("TODO: Implement china-specific scene opcode %d (%s)", op._opCode,
-			_sceneOpCodeName(op._opCode).c_str());
-		break;
-	}
-	return false;
-}
-
-bool Scene::runBeamishOp(const SceneOp &op) {
-	DgdsEngine *engine = DgdsEngine::getInstance();
-
-	if (op._opCode & kSceneOpHasConditionalOpsFlag) {
-		uint16 opcode = op._opCode & ~kSceneOpHasConditionalOpsFlag;
-		for (const ConditionalSceneOp &cop : engine->getScene()->getConditionalOps()) {
-			if (cop._opCode == opcode && checkConditions(cop._conditionList)) {
-				if (!runOps(cop._opList))
-					return true;
-			}
-		}
-		return false;
-	}
-
-	switch (op._opCode) {
-	case kSceneOpOpenBeamishGameOverMenu:
-		engine->setMenuToTrigger(kMenuGameOver);
-		break;
-	case kSceneOpOpenBeamishOpenSkipCreditsMenu:
-		engine->setMenuToTrigger(kMenuSkipPlayIntro);
-		break;
-	default:
-		warning("TODO: Implement beamish-specific scene opcode %d", op._opCode);
-		break;
-	}
-	return false;
-}
-
 //
 // Note: ops list here is not a reference on purpose, it must be copied.
 // The underlying list might be freed during execution if the scene changes, but
@@ -884,24 +485,8 @@ bool Scene::runOps(const Common::Array<SceneOp> ops, int16 addMinuites /* = 0 */
 			engine->getClock().addGameTime(addMinuites);
 			addMinuites = 0;
 		}
-		if (op._opCode < 100) {
-			sceneChanged = runSceneOp(op);
-		} else {
-			// Game-specific opcode
-			switch (engine->getGameId()) {
-			case GID_DRAGON:
-				sceneChanged = runDragonOp(op);
-				break;
-			case GID_HOC:
-				sceneChanged = runChinaOp(op);
-				break;
-			case GID_WILLY:
-				sceneChanged = runBeamishOp(op);
-				break;
-			default:
-				error("TODO: Implement game-specific scene op for this game");
-			}
-		}
+
+		sceneChanged = op.runOp();
 
 		if (sceneChanged)
 			break;
@@ -1072,15 +657,15 @@ void SDSScene::unload() {
 
 Common::String SDSScene::dump(const Common::String &indent) const {
 	Common::String str = Common::String::format("%sSDSScene<num %d %d ads %s", indent.c_str(), _num, _field6_0x14, _adsFile.c_str());
-	str += _dumpStructList(indent, "enterSceneOps", _enterSceneOps);
-	str += _dumpStructList(indent, "leaveSceneOps", _leaveSceneOps);
-	str += _dumpStructList(indent, "preTickOps", _preTickOps);
-	str += _dumpStructList(indent, "postTickOps", _postTickOps);
-	str += _dumpStructList(indent, "hotAreaList", _hotAreaList);
-	str += _dumpStructList(indent, "objInteractions1", _objInteractions1);
-	str += _dumpStructList(indent, "objInteractions2", _objInteractions2);
-	str += _dumpStructList(indent, "dialogues", _dialogs);
-	str += _dumpStructList(indent, "triggers", _triggers);
+	str += DebugUtil::dumpStructList(indent, "enterSceneOps", _enterSceneOps);
+	str += DebugUtil::dumpStructList(indent, "leaveSceneOps", _leaveSceneOps);
+	str += DebugUtil::dumpStructList(indent, "preTickOps", _preTickOps);
+	str += DebugUtil::dumpStructList(indent, "postTickOps", _postTickOps);
+	str += DebugUtil::dumpStructList(indent, "hotAreaList", _hotAreaList);
+	str += DebugUtil::dumpStructList(indent, "objInteractions1", _objInteractions1);
+	str += DebugUtil::dumpStructList(indent, "objInteractions2", _objInteractions2);
+	str += DebugUtil::dumpStructList(indent, "dialogues", _dialogs);
+	str += DebugUtil::dumpStructList(indent, "triggers", _triggers);
 
 	str += "\n";
 	str += indent + ">";
@@ -1348,6 +933,8 @@ void SDSScene::loadTalkDataAndSetFlags(uint16 talknum, uint16 headnum) {
 			for (auto &head : data._heads) {
 				if (head._num != headnum)
 					continue;
+
+				_conversation._drawRect = head._rect;
 				head._flags = static_cast<HeadFlags>(head._flags & ~(kHeadFlag1 | kHeadFlag10));
 				head._flags = static_cast<HeadFlags>(head._flags | (kHeadFlag8 | kHeadFlagVisible));
 				break;
@@ -2240,15 +1827,15 @@ bool GDSScene::parse(Common::SeekableReadStream *stream) {
 
 Common::String GDSScene::dump(const Common::String &indent) const {
 	Common::String str = Common::String::format("%sGDSScene<icons %s", indent.c_str(), _iconFile.c_str());
-	str += _dumpStructList(indent, "gameItems", _gameItems);
-	str += _dumpStructList(indent, "startGameOps", _startGameOps);
-	str += _dumpStructList(indent, "quitGameOps", _quitGameOps);
-	str += _dumpStructList(indent, "preTickOps", _preTickOps);
-	str += _dumpStructList(indent, "postTickOps", _postTickOps);
-	str += _dumpStructList(indent, "onChangeSceneOps", _onChangeSceneOps);
-	str += _dumpStructList(indent, "perSceneGlobals", _perSceneGlobals);
-	str += _dumpStructList(indent, "objInteractions1", _objInteractions1);
-	str += _dumpStructList(indent, "objInteractions2", _objInteractions2);
+	str += DebugUtil::dumpStructList(indent, "gameItems", _gameItems);
+	str += DebugUtil::dumpStructList(indent, "startGameOps", _startGameOps);
+	str += DebugUtil::dumpStructList(indent, "quitGameOps", _quitGameOps);
+	str += DebugUtil::dumpStructList(indent, "preTickOps", _preTickOps);
+	str += DebugUtil::dumpStructList(indent, "postTickOps", _postTickOps);
+	str += DebugUtil::dumpStructList(indent, "onChangeSceneOps", _onChangeSceneOps);
+	str += DebugUtil::dumpStructList(indent, "perSceneGlobals", _perSceneGlobals);
+	str += DebugUtil::dumpStructList(indent, "objInteractions1", _objInteractions1);
+	str += DebugUtil::dumpStructList(indent, "objInteractions2", _objInteractions2);
 
 	str += "\n";
 	str += indent + ">";
diff --git a/engines/dgds/scene.h b/engines/dgds/scene.h
index 40f61793845..944e84aa411 100644
--- a/engines/dgds/scene.h
+++ b/engines/dgds/scene.h
@@ -30,6 +30,8 @@
 #include "dgds/head.h"
 #include "dgds/dgds_rect.h"
 #include "dgds/minigames/shell_game.h"
+#include "dgds/scene_condition.h"
+#include "dgds/scene_op.h"
 
 namespace Dgds {
 
@@ -40,32 +42,6 @@ class SoundRaw;
 class TTMInterpreter;
 class TTMEnviro;
 
-enum SceneCondition {
-	kSceneCondNone = 0,
-	kSceneCondLessThan = 1,
-	kSceneCondEqual = 2,
-	kSceneCondNegate = 4,
-	kSceneCondAbsVal = 8,
-	kSceneCondOr = 0x10,
-	kSceneCondNeedItemSceneNum = 0x20,
-	kSceneCondNeedItemQuality = 0x40,
-	kSceneCondSceneState = 0x80
-};
-
-class SceneConditions {
-public:
-	SceneConditions(uint16 num, SceneCondition cond, int16 val) : _num(num), _flags(cond), _val(val) {}
-	Common::String dump(const Common::String &indent) const;
-
-	uint16 getNum() const { return _num; }
-	SceneCondition getCond() const { return _flags; }
-	int16 getVal() const { return _val; }
-
-private:
-	uint16 _num;
-	SceneCondition _flags; /* eg, see usage in FUN_1f1a_2106 */
-	int16 _val;
-};
 
 class HotArea {
 public:
@@ -94,93 +70,6 @@ public:
 	DgdsRect _rect;
 };
 
-enum SceneOpCode {
-	kSceneOpNone = 0,
-	kSceneOpChangeScene = 1,  	// args: scene num
-	kSceneOpNoop = 2,		 	// args: none. Maybe should close dialogue?
-	kSceneOpGlobal = 3,			// args: array of uints
-	kSceneOpSegmentStateOps = 4,	// args: array of uint pairs [op seg, op seg], term with 0,0 that modify segment states
-	kSceneOpSetItemAttr = 5,	// args: [item num, item param 0x28, item param 0x2c]. set item attrs?
-	kSceneOpSetDragItem = 6,		// args: item num. give item?
-	kSceneOpOpenInventory = 7,	// args: none.
-	kSceneOpShowDlg = 8,		// args: dialogue number.
-	kSceneOpShowInvButton = 9,		// args: none.
-	kSceneOpHideInvButton = 10,	// args: none.
-	kSceneOpEnableTrigger = 11,	// args: trigger num
-	kSceneOpChangeSceneToStored = 12,	// args: none. Change scene to stored number
-	kSceneOpAddFlagToDragItem = 13,			// args: none.
-	kSceneOpOpenInventoryZoom = 14,	// args: none.
-	kSceneOpMoveItemsBetweenScenes = 15,	// args: none.
-	kSceneOpShowClock = 16,		// args: none.  set clock script-visible.
-	kSceneOpHideClock = 17,		// args: none.  set clock script-hidden.
-	kSceneOpShowMouse = 18,		// args: none.
-	kSceneOpHideMouse = 19,		// args: none.
-	// Op 20 onward are common, but not in dragon
-
-	kSceneOpLoadTalkDataAndSetFlags = 20, // args: tdsnum to load, headnum
-	kSceneOpDrawVisibleTalkHeads = 21, // args: none
-	kSceneOpLoadTalkData = 22, 	// args: tds num to load
-	kSceneOpLoadDDSData = 24, 	// args: dds num to load
-	kSceneOpFreeDDSData = 25,	// args: dds num to free
-	kSceneOpFreeTalkData = 26, 	// args: tds num to free
-
-	// Dragon-specific opcodes
-	kSceneOpPasscode = 100,			// args: none.
-	kSceneOpMeanwhile = 101,	// args: none. Clears screen and displays "meanwhile".
-	kSceneOpOpenGameOverMenu = 102,	// args: none.
-	kSceneOpTiredDialog = 103,			// args: none. Something about "boy am I tired"?
-	kSceneOpArcadeTick = 104,			// args: none. Called in arcade post-tick.
-	kSceneOpDrawDragonCountdown1 = 105,			// args: none. Draw special countdown number at 141, 56
-	kSceneOpDrawDragonCountdown2 = 106,			// args: none. Draw some number at 250, 42
-	kSceneOpOpenPlaySkipIntroMenu = 107, // args: none.  DRAGON: Show menu 50, the "Play Introduction" / "Skip Introduction" menu.
-	kSceneOpOpenBetterSaveGameMenu = 108,			// args: none. DRAGON: Show menu 46, the "Before arcade maybe you better save your game" menu.
-
-	// China-specific opcodes
-	kSceneOpChinaTankInit = 100,
-	kSceneOpChinaTankEnd = 101,
-	kSceneOpChinaTankTick = 102,
-	kSceneOpChinaSetLanding = 103,
-	kSceneOpChinaScrollIntro = 104,
-	kSceneOpChinaScrollLeft = 105,
-	kSceneOpChinaScrollRight = 107,
-	kSceneOpShellGameInit = 108,
-	kSceneOpShellGameEnd = 109,
-	kSceneOpShellGameTick = 110,
-	kSceneOpChinaTrainInit = 111,
-	kSceneOpChinaTrainEnd = 112,
-	kSceneOpChinaTrainTick = 113,
-	kSceneOpChinaOpenGameOverMenu = 114,	// args: none.
-	kSceneOpChinaOpenSkipCreditsMenu = 115,	// args: none.
-	kSceneOpChinaOnIntroTick = 116,	// args: none.
-	kSceneOpChinaOnIntroInit = 117,	// args: none.
-	kSceneOpChinaOnIntroEnd = 118,	// args: none.
-
-	// Beamish-specific opcodes
-	kSceneOpOpenBeamishGameOverMenu = 100,
-	kSceneOpOpenBeamishOpenSkipCreditsMenu = 101,
-
-	kSceneOpMaxCode = 255, // for checking file load
-
-	kSceneOpHasConditionalOpsFlag = 0x8000,
-};
-
-class SceneOp {
-public:
-	Common::Array<SceneConditions> _conditionList;
-	Common::Array<uint16> _args;
-	SceneOpCode _opCode;
-
-	Common::String dump(const Common::String &indent) const;
-};
-
-class ConditionalSceneOp {
-public:
-	uint _opCode;
-	Common::Array<SceneConditions> _conditionList;
-	Common::Array<SceneOp> _opList;
-
-	Common::String dump(const Common::String &indent) const;
-};
 
 class GameItem : public HotArea {
 public:
@@ -289,6 +178,13 @@ public:
 
 	virtual Common::Error syncState(Common::Serializer &s) = 0;
 
+	// These are all static as they are potentially run over scene changes.
+	static bool checkConditions(const Common::Array<SceneConditions> &cond);
+
+	static void segmentStateOps(const Common::Array<uint16> &args);
+	static void setItemAttrOp(const Common::Array<uint16> &args);
+	static void setDragItemOp(const Common::Array<uint16> &args);
+
 protected:
 	bool readConditionList(Common::SeekableReadStream *s, Common::Array<SceneConditions> &list) const;
 	bool readHotArea(Common::SeekableReadStream *s, HotArea &dst) const;
@@ -302,17 +198,6 @@ protected:
 	bool readDialogActionList(Common::SeekableReadStream *s, Common::Array<DialogAction> &list) const;
 	bool readConditionalSceneOpList(Common::SeekableReadStream *s, Common::Array<ConditionalSceneOp> &list) const;
 
-	static void segmentStateOps(const Common::Array<uint16> &args);
-	static void setItemAttrOp(const Common::Array<uint16> &args);
-	static void setDragItemOp(const Common::Array<uint16> &args);
-
-	// These are all static as they are potentially run over scene changes.
-	static bool checkConditions(const Common::Array<SceneConditions> &cond);
-	static bool runSceneOp(const SceneOp &op);
-	static bool runDragonOp(const SceneOp &op);
-	static bool runChinaOp(const SceneOp &op);
-	static bool runBeamishOp(const SceneOp &op);
-
 	uint32 _magic;
 	Common::String _version;
 
diff --git a/engines/dgds/scene_condition.cpp b/engines/dgds/scene_condition.cpp
new file mode 100644
index 00000000000..2129e5c61c5
--- /dev/null
+++ b/engines/dgds/scene_condition.cpp
@@ -0,0 +1,62 @@
+/* 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 "dgds/scene_condition.h"
+
+namespace Dgds {
+
+
+Common::String _sceneConditionStr(SceneCondition cflag) {
+	Common::String ret;
+
+	if (cflag & kSceneCondOr)
+		return "or";
+
+	if (cflag & kSceneCondSceneState)
+		ret += "state|";
+	if (cflag & kSceneCondNeedItemSceneNum)
+		ret += "itemsnum|";
+	if (cflag & kSceneCondNeedItemQuality)
+		ret += "quality|";
+	if ((cflag & (kSceneCondSceneState | kSceneCondNeedItemSceneNum | kSceneCondNeedItemQuality)) == 0)
+		ret += "global|";
+
+	cflag = static_cast<SceneCondition>(cflag & ~(kSceneCondSceneState | kSceneCondNeedItemSceneNum | kSceneCondNeedItemQuality));
+	if (cflag == kSceneCondNone)
+		ret += "nocond";
+	if (cflag & kSceneCondLessThan)
+		ret += "less";
+	if (cflag & kSceneCondEqual)
+		ret += "equal";
+	if (cflag & kSceneCondNegate)
+		ret += "-not";
+	if (cflag & kSceneCondAbsVal)
+		ret += "(abs)";
+
+	return ret;
+}
+
+Common::String SceneConditions::dump(const Common::String &indent) const {
+	return Common::String::format("%sSceneCondition<flg 0x%02x(%s) num %d val %d>", indent.c_str(),
+			_flags, _sceneConditionStr(_flags).c_str(), _num, _val);
+}
+
+} // end namespace Dgds
diff --git a/engines/dgds/scene_condition.h b/engines/dgds/scene_condition.h
new file mode 100644
index 00000000000..5324644efe5
--- /dev/null
+++ b/engines/dgds/scene_condition.h
@@ -0,0 +1,59 @@
+/* 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 DGDS_SCENE_CONDITION_H
+#define DGDS_SCENE_CONDITION_H
+
+#include "common/types.h"
+#include "common/str.h"
+
+namespace Dgds {
+
+enum SceneCondition {
+	kSceneCondNone = 0,
+	kSceneCondLessThan = 1,
+	kSceneCondEqual = 2,
+	kSceneCondNegate = 4,
+	kSceneCondAbsVal = 8,
+	kSceneCondOr = 0x10,
+	kSceneCondNeedItemSceneNum = 0x20,
+	kSceneCondNeedItemQuality = 0x40,
+	kSceneCondSceneState = 0x80
+};
+
+class SceneConditions {
+public:
+	SceneConditions(uint16 num, SceneCondition cond, int16 val) : _num(num), _flags(cond), _val(val) {}
+	Common::String dump(const Common::String &indent) const;
+
+	uint16 getNum() const { return _num; }
+	SceneCondition getCond() const { return _flags; }
+	int16 getVal() const { return _val; }
+
+private:
+	uint16 _num;
+	SceneCondition _flags; /* eg, see usage in FUN_1f1a_2106 */
+	int16 _val;
+};
+
+} // end namespace Dgds
+
+#endif // DGDS_SCENE_CONDITION_H
diff --git a/engines/dgds/scene_op.cpp b/engines/dgds/scene_op.cpp
new file mode 100644
index 00000000000..b8878bc1471
--- /dev/null
+++ b/engines/dgds/scene_op.cpp
@@ -0,0 +1,399 @@
+/* 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 "graphics/cursorman.h"
+
+#include "dgds/scene_op.h"
+#include "dgds/dgds.h"
+#include "dgds/debug_util.h"
+#include "dgds/globals.h"
+#include "dgds/inventory.h"
+#include "dgds/scene.h"
+#include "dgds/dragon_native.h"
+#include "dgds/hoc_intro.h"
+#include "dgds/minigames/dragon_arcade.h"
+#include "dgds/minigames/china_train.h"
+#include "dgds/minigames/china_tank.h"
+
+namespace Dgds {
+
+static Common::String _sceneOpCodeName(SceneOpCode code) {
+	switch (code) {
+	case kSceneOpNone: 		  	return "none";
+	case kSceneOpChangeScene: 	return "changeScene";
+	case kSceneOpNoop:		  	return "noop";
+	case kSceneOpGlobal:		return "global";
+	case kSceneOpSegmentStateOps: return "sceneOpSegmentStateOps";
+	case kSceneOpSetItemAttr:   return "setItemAttr";
+	case kSceneOpSetDragItem:   return "setDragItem";
+	case kSceneOpOpenInventory: return "openInventory";
+	case kSceneOpShowDlg:		return "showdlg";
+	case kSceneOpShowInvButton:	return "showInvButton";
+	case kSceneOpHideInvButton:	return "hideInvButton";
+	case kSceneOpEnableTrigger: return "enabletrigger";
+	case kSceneOpChangeSceneToStored: 	return "changeSceneToStored";
+	case kSceneOpAddFlagToDragItem:		return "addFlagToDragItem";
+	case kSceneOpMoveItemsBetweenScenes: return "moveItemsBetweenScenes";
+	case kSceneOpOpenInventoryZoom:   	return "openInventoryZoom";
+	case kSceneOpShowClock:		return "sceneOpShowClock";
+	case kSceneOpHideClock:		return "sceneOpHideClock";
+	case kSceneOpShowMouse:		return "sceneOpShowMouse";
+	case kSceneOpHideMouse:		return "sceneOpHideMouse";
+	case kSceneOpLoadTalkDataAndSetFlags: return "sceneOpLoadTalkDataAndSetFlags";
+	case kSceneOpDrawVisibleTalkHeads: return "sceneOpDrawVisibleTalksHeads";
+	case kSceneOpLoadTalkData: 	return "sceneOpLoadTalkData";
+	case kSceneOpLoadDDSData: 	return "sceneOpLoadDDSData";
+	case kSceneOpFreeDDSData: 	return "sceneOpFreeDDSData";
+	case kSceneOpFreeTalkData: 	return "sceneOpFreeTalkData";
+
+	default:
+		break;
+	}
+
+	if (DgdsEngine::getInstance()->getGameId() == GID_DRAGON) {
+		switch (code) {
+		case kSceneOpPasscode:		return "passcode";
+		case kSceneOpMeanwhile:   	return "meanwhile";
+		case kSceneOpOpenGameOverMenu: return "openGameOverMenu";
+		case kSceneOpTiredDialog:	return "openTiredDialog";
+		case kSceneOpArcadeTick: 	return "sceneOpArcadeTick";
+		case kSceneOpDrawDragonCountdown1: 	return "drawDragonCountdown1";
+		case kSceneOpDrawDragonCountdown2:	return "drawDragonCountdown2";
+		case kSceneOpOpenPlaySkipIntroMenu: return "openPlaySkipIntroMovie";
+		case kSceneOpOpenBetterSaveGameMenu: return "openBetterSaveGameMenu";
+		default:
+			break;
+		}
+	} else if (DgdsEngine::getInstance()->getGameId() == GID_HOC) {
+		switch (code) {
+		case kSceneOpChinaTankInit:			return "tankInit";
+		case kSceneOpChinaTankEnd:			return "tankEnd";
+		case kSceneOpChinaTankTick:			return "tankTick";
+		case kSceneOpChinaScrollLeft:		return "scrollLeft";
+		case kSceneOpChinaScrollRight:		return "scrollRight";
+		case kSceneOpShellGameInit:			return "shellGameInit";
+		case kSceneOpShellGameEnd:			return "shellGameEnd";
+		case kSceneOpShellGameTick:			return "shellGameTick";
+		case kSceneOpChinaTrainInit:		return "trainInit";
+		case kSceneOpChinaTrainEnd:			return "trainEnd";
+		case kSceneOpChinaTrainTick:		return "trainTick";
+		case kSceneOpChinaOpenGameOverMenu: return "gameOverMenu";
+		case kSceneOpChinaOpenSkipCreditsMenu: return "skipCreditsMenu";
+		case kSceneOpChinaOnIntroInit:		return "chinaOnIntroInit";
+		case kSceneOpChinaOnIntroTick:		return "chinaOnIntroTick";
+		case kSceneOpChinaOnIntroEnd:  		return "chinaOnIntroEnd";
+		default:
+			break;
+		}
+	} else if (DgdsEngine::getInstance()->getGameId() == GID_WILLY) {
+		switch (code) {
+		case kSceneOpOpenBeamishGameOverMenu: return "openGameOverMenu";
+		case kSceneOpOpenBeamishOpenSkipCreditsMenu: return "skipCreditsMenu";
+		default:
+			break;
+		}
+	}
+
+	return Common::String::format("sceneOp%d", (int)code);
+}
+
+
+Common::String SceneOp::dump(const Common::String &indent) const {
+	Common::String argsStr;
+	if (_args.empty()) {
+		argsStr = "[]";
+	} else {
+		argsStr = "[";
+		for  (uint i : _args)
+			argsStr += Common::String::format("%d ", i);
+		argsStr.setChar(']', argsStr.size() - 1);
+	}
+	Common::String str = Common::String::format("%sSceneOp<op: %s args: %s", indent.c_str(), _sceneOpCodeName(_opCode).c_str(), argsStr.c_str());
+
+	str += DebugUtil::dumpStructList(indent, "conditionList", _conditionList);
+	if (!_conditionList.empty()) {
+		str += "\n";
+		str += indent;
+	}
+	str += ">";
+	return str;
+}
+
+bool SceneOp::runOp() const {
+	bool sceneChanged;
+
+	if (_opCode < 100) {
+		sceneChanged = runCommonOp();
+	} else {
+		// Game-specific opcode
+		switch (DgdsEngine::getInstance()->getGameId()) {
+		case GID_DRAGON:
+			sceneChanged = runDragonOp();
+			break;
+		case GID_HOC:
+			sceneChanged = runChinaOp();
+			break;
+		case GID_WILLY:
+			sceneChanged = runBeamishOp();
+			break;
+		default:
+			error("TODO: Implement game-specific scene op for this game");
+		}
+	}
+	return sceneChanged;
+}
+
+bool SceneOp::runCommonOp() const {
+	DgdsEngine *engine = DgdsEngine::getInstance();
+	switch (_opCode) {
+	case kSceneOpChangeScene:
+		if (engine->changeScene(_args[0]))
+			return true;
+		break;
+	case kSceneOpNoop:
+		break;
+	case kSceneOpGlobal:
+		// The globals are held by the GDS scene
+		engine->getGDSScene()->globalOps(_args);
+		break;
+	case kSceneOpSegmentStateOps:
+		SDSScene::segmentStateOps(_args);
+		break;
+	case kSceneOpSetItemAttr:
+		SDSScene::setItemAttrOp(_args);
+		break;
+	case kSceneOpSetDragItem:
+		SDSScene::setDragItemOp(_args);
+		break;
+	case kSceneOpOpenInventory:
+		engine->getInventory()->open();
+		// This implicitly changes scene num
+		break;
+	case kSceneOpShowDlg:
+		if (_args.size() == 1)
+			engine->getScene()->showDialog(0, _args[0]);
+		else if (_args.size() > 1)
+			engine->getScene()->showDialog(_args[0], _args[1]);
+		break;
+	case kSceneOpShowInvButton:
+		engine->getScene()->addInvButtonToHotAreaList();
+		break;
+	case kSceneOpHideInvButton:
+		engine->getScene()->removeInvButtonFromHotAreaList();
+		break;
+	case kSceneOpEnableTrigger:
+		engine->getScene()->enableTrigger(_args[0]);
+		break;
+	case kSceneOpChangeSceneToStored: {
+		int16 sceneNo = engine->getGameGlobals()->getGlobal(0x61);
+		if (engine->changeScene(sceneNo))
+			return true;
+		break;
+	}
+	case kSceneOpAddFlagToDragItem: {
+		GameItem *item = engine->getScene()->getDragItem();
+		if (item) {
+			item->_flags |= 1;
+			// TODO: Use hot x/y or just position?
+			Common::Point lastMouse = engine->getLastMouseMinusHot();
+			item->_rect.x = lastMouse.x;
+			item->_rect.y = lastMouse.y;
+		}
+		break;
+	}
+	case kSceneOpOpenInventoryZoom:
+		engine->getInventory()->setShowZoomBox(true);
+		engine->getInventory()->open();
+		return true;
+	case kSceneOpMoveItemsBetweenScenes: {
+		int16 fromScene = engine->getGameGlobals()->getGlobal(0x55);
+		int16 toScene = engine->getGameGlobals()->getGlobal(0x54);
+		for (auto &item : engine->getGDSScene()->getGameItems()) {
+			if (item._inSceneNum == fromScene)
+				item._inSceneNum = toScene;
+		}
+		break;
+	}
+	case kSceneOpShowClock:
+		engine->setShowClock(true);
+		break;
+	case kSceneOpHideClock:
+		engine->setShowClock(false);
+		break;
+	case kSceneOpShowMouse:
+		CursorMan.showMouse(true);
+		break;
+	case kSceneOpHideMouse:
+		CursorMan.showMouse(false);
+		break;
+	case kSceneOpLoadTalkDataAndSetFlags: // args: tdsnum to load, headnum
+		engine->getScene()->loadTalkDataAndSetFlags(_args[0], _args[1]);
+		break;
+	case kSceneOpDrawVisibleTalkHeads: // args: none
+		engine->getScene()->updateVisibleTalkers();
+		break;
+	case kSceneOpLoadTalkData: 	// args: tds num to load
+		engine->getScene()->loadTalkData(_args[0]);
+		break;
+	case kSceneOpLoadDDSData: 	// args: dds num to load
+		if (_args[0])
+			engine->getScene()->loadDialogData(_args[0]);
+		break;
+	case kSceneOpFreeDDSData:	// args: dds num to free
+		engine->getScene()->freeDialogData(_args[0]);
+		break;
+	case kSceneOpFreeTalkData: 	// args: tds num to free
+		engine->getScene()->freeTalkData(_args[0]);
+		break;
+
+	default:
+		warning("TODO: Implement generic scene op %d", _opCode);
+		break;
+	}
+	return false;
+}
+
+bool SceneOp::runDragonOp() const {
+	DgdsEngine *engine = DgdsEngine::getInstance();
+	switch (_opCode) {
+	case kSceneOpPasscode:
+		DragonNative::updatePasscodeGlobal();
+		break;
+	case kSceneOpMeanwhile:
+		// TODO: Should we draw "meanwhile" like the original? it just gets overwritten with the image anyway.
+		// Probably need to do something here to avoid flashing..
+		//engine->_compositionBuffer.fillRect(Common::Rect(SCREEN_WIDTH, SCREEN_HEIGHT), 0);
+		break;
+	case kSceneOpOpenGameOverMenu:
+		engine->setMenuToTrigger(kMenuGameOver);
+		break;
+	case kSceneOpTiredDialog:
+		engine->getInventory()->close();
+		engine->getScene()->addAndShowTiredDialog();
+		break;
+	case kSceneOpArcadeTick:
+		// TODO: Add a configuration option to skip arcade sequence?
+		// g_system->displayMessageOnOSD(_("Skipping DGDS arcade sequence"));
+		// engine->getGameGlobals()->setGlobal(0x21, 6);
+		engine->getDragonArcade()->arcadeTick();
+		break;
+	case kSceneOpDrawDragonCountdown1:
+		DragonNative::drawCountdown(FontManager::k4x5Font, 141, 56);
+		break;
+	case kSceneOpDrawDragonCountdown2:
+		DragonNative::drawCountdown(FontManager::k8x8Font, 250, 42);
+		break;
+	case kSceneOpOpenPlaySkipIntroMenu:
+		engine->setMenuToTrigger(kMenuSkipPlayIntro);
+		break;
+	case kSceneOpOpenBetterSaveGameMenu:
+		engine->setMenuToTrigger(kMenuSaveBeforeArcade);
+		break;
+	default:
+		error("Unexpected Dragon scene opcode %d", _opCode);
+		break;
+	}
+	return false;
+}
+
+bool SceneOp::runChinaOp() const {
+	DgdsEngine *engine = DgdsEngine::getInstance();
+	switch (_opCode) {
+	case kSceneOpChinaTankInit:
+		engine->getChinaTank()->init();
+		break;
+	case kSceneOpChinaTankEnd:
+		engine->getChinaTank()->end();
+		break;
+	case kSceneOpChinaTankTick:
+		engine->getChinaTank()->tick();
+		break;
+	case kSceneOpShellGameTick:
+		engine->getShellGame()->shellGameTick();
+		break;
+	case kSceneOpShellGameEnd:
+		engine->getShellGame()->shellGameEnd();
+		break;
+	case kSceneOpChinaTrainInit:
+		engine->getChinaTrain()->init();
+		break;
+	case kSceneOpChinaTrainEnd:
+		engine->getChinaTrain()->end();
+		break;
+	case kSceneOpChinaTrainTick:
+		engine->getChinaTrain()->tick();
+		break;
+	case kSceneOpChinaOpenGameOverMenu:
+		engine->setMenuToTrigger(kMenuGameOver);
+		break;
+	case kSceneOpChinaOpenSkipCreditsMenu:
+		engine->setMenuToTrigger(kMenuSkipPlayIntro);
+		break;
+	case kSceneOpChinaOnIntroInit:
+		engine->getHocIntro()->init();
+		break;
+	case kSceneOpChinaOnIntroTick:
+		engine->getHocIntro()->tick();
+		break;
+	case kSceneOpChinaOnIntroEnd:
+		engine->getHocIntro()->end();
+		break;
+	case kSceneOpChinaScrollIntro:
+	case kSceneOpChinaScrollLeft:
+	case kSceneOpChinaScrollRight:
+		// These map to null functions.
+		break;
+	default:
+		warning("TODO: Implement china-specific scene opcode: (%s)", dump("").c_str());
+		break;
+	}
+	return false;
+}
+
+bool SceneOp::runBeamishOp() const {
+	DgdsEngine *engine = DgdsEngine::getInstance();
+
+	if (_opCode & kSceneOpHasConditionalOpsFlag) {
+		uint16 opcode = _opCode & ~kSceneOpHasConditionalOpsFlag;
+		for (const ConditionalSceneOp &cop : engine->getScene()->getConditionalOps()) {
+			if (cop._opCode == opcode && engine->getScene()->checkConditions(cop._conditionList)) {
+				if (!Scene::runOps(cop._opList))
+					return true;
+			}
+		}
+		return false;
+	}
+
+	switch (_opCode) {
+	case kSceneOpOpenBeamishGameOverMenu:
+		engine->setMenuToTrigger(kMenuGameOver);
+		break;
+	case kSceneOpOpenBeamishOpenSkipCreditsMenu:
+		engine->setMenuToTrigger(kMenuSkipPlayIntro);
+		break;
+	default:
+		warning("TODO: Implement beamish-specific scene opcode %d", _opCode);
+		break;
+	}
+	return false;
+}
+
+} // end namespace Dgds
diff --git a/engines/dgds/scene_op.h b/engines/dgds/scene_op.h
new file mode 100644
index 00000000000..d7e0a0f3b3f
--- /dev/null
+++ b/engines/dgds/scene_op.h
@@ -0,0 +1,130 @@
+/* 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 DGDS_SCENE_OP_H
+#define DGDS_SCENE_OP_H
+
+#include "common/types.h"
+#include "common/array.h"
+
+#include "dgds/scene_condition.h"
+
+namespace Dgds {
+
+enum SceneOpCode {
+	kSceneOpNone = 0,
+	kSceneOpChangeScene = 1,  	// args: scene num
+	kSceneOpNoop = 2,		 	// args: none. Maybe should close dialogue?
+	kSceneOpGlobal = 3,			// args: array of uints
+	kSceneOpSegmentStateOps = 4,	// args: array of uint pairs [op seg, op seg], term with 0,0 that modify segment states
+	kSceneOpSetItemAttr = 5,	// args: [item num, item param 0x28, item param 0x2c]. set item attrs?
+	kSceneOpSetDragItem = 6,		// args: item num. give item?
+	kSceneOpOpenInventory = 7,	// args: none.
+	kSceneOpShowDlg = 8,		// args: dialogue number.
+	kSceneOpShowInvButton = 9,		// args: none.
+	kSceneOpHideInvButton = 10,	// args: none.
+	kSceneOpEnableTrigger = 11,	// args: trigger num
+	kSceneOpChangeSceneToStored = 12,	// args: none. Change scene to stored number
+	kSceneOpAddFlagToDragItem = 13,			// args: none.
+	kSceneOpOpenInventoryZoom = 14,	// args: none.
+	kSceneOpMoveItemsBetweenScenes = 15,	// args: none.
+	kSceneOpShowClock = 16,		// args: none.  set clock script-visible.
+	kSceneOpHideClock = 17,		// args: none.  set clock script-hidden.
+	kSceneOpShowMouse = 18,		// args: none.
+	kSceneOpHideMouse = 19,		// args: none.
+	// Op 20 onward are common, but not in dragon
+
+	kSceneOpLoadTalkDataAndSetFlags = 20, // args: tdsnum to load, headnum
+	kSceneOpDrawVisibleTalkHeads = 21, // args: none
+	kSceneOpLoadTalkData = 22, 	// args: tds num to load
+	kSceneOpLoadDDSData = 24, 	// args: dds num to load
+	kSceneOpFreeDDSData = 25,	// args: dds num to free
+	kSceneOpFreeTalkData = 26, 	// args: tds num to free
+
+	// Dragon-specific opcodes
+	kSceneOpPasscode = 100,			// args: none.
+	kSceneOpMeanwhile = 101,	// args: none. Clears screen and displays "meanwhile".
+	kSceneOpOpenGameOverMenu = 102,	// args: none.
+	kSceneOpTiredDialog = 103,			// args: none. Something about "boy am I tired"?
+	kSceneOpArcadeTick = 104,			// args: none. Called in arcade post-tick.
+	kSceneOpDrawDragonCountdown1 = 105,			// args: none. Draw special countdown number at 141, 56
+	kSceneOpDrawDragonCountdown2 = 106,			// args: none. Draw some number at 250, 42
+	kSceneOpOpenPlaySkipIntroMenu = 107, // args: none.  DRAGON: Show menu 50, the "Play Introduction" / "Skip Introduction" menu.
+	kSceneOpOpenBetterSaveGameMenu = 108,			// args: none. DRAGON: Show menu 46, the "Before arcade maybe you better save your game" menu.
+
+	// China-specific opcodes
+	kSceneOpChinaTankInit = 100,
+	kSceneOpChinaTankEnd = 101,
+	kSceneOpChinaTankTick = 102,
+	kSceneOpChinaSetLanding = 103,
+	kSceneOpChinaScrollIntro = 104,
+	kSceneOpChinaScrollLeft = 105,
+	kSceneOpChinaScrollRight = 107,
+	kSceneOpShellGameInit = 108,
+	kSceneOpShellGameEnd = 109,
+	kSceneOpShellGameTick = 110,
+	kSceneOpChinaTrainInit = 111,
+	kSceneOpChinaTrainEnd = 112,
+	kSceneOpChinaTrainTick = 113,
+	kSceneOpChinaOpenGameOverMenu = 114,	// args: none.
+	kSceneOpChinaOpenSkipCreditsMenu = 115,	// args: none.
+	kSceneOpChinaOnIntroTick = 116,	// args: none.
+	kSceneOpChinaOnIntroInit = 117,	// args: none.
+	kSceneOpChinaOnIntroEnd = 118,	// args: none.
+
+	// Beamish-specific opcodes
+	kSceneOpOpenBeamishGameOverMenu = 100,
+	kSceneOpOpenBeamishOpenSkipCreditsMenu = 101,
+
+	kSceneOpMaxCode = 255, // for checking file load
+
+	kSceneOpHasConditionalOpsFlag = 0x8000,
+};
+
+class SceneOp {
+public:
+	Common::Array<SceneConditions> _conditionList;
+	Common::Array<uint16> _args;
+	SceneOpCode _opCode;
+
+	Common::String dump(const Common::String &indent) const;
+	bool runOp() const;
+
+private:
+	bool runCommonOp() const;
+	bool runDragonOp() const;
+	bool runChinaOp() const;
+	bool runBeamishOp() const;
+};
+
+class ConditionalSceneOp {
+public:
+	uint _opCode;
+	Common::Array<SceneConditions> _conditionList;
+	Common::Array<SceneOp> _opList;
+
+	Common::String dump(const Common::String &indent) const;
+};
+
+
+} // end namespace Dgds
+
+#endif // DGDS_SCENE_OP_H
diff --git a/engines/dgds/ttm.cpp b/engines/dgds/ttm.cpp
index f6c4d97fe28..00a620fcc1d 100644
--- a/engines/dgds/ttm.cpp
+++ b/engines/dgds/ttm.cpp
@@ -627,7 +627,11 @@ bool TTMInterpreter::handleOperation(TTMEnviro &env, TTMSeq &seq, uint16 op, byt
 		env._getPuts[seq._currentGetPutId].reset();
 		break;
 	case 0x0110: // PURGE void
-		_vm->adsInterpreter()->setHitTTMOp0110();
+		// only set if not running from CDS script
+		if (env._cdsSeqNum < 0)
+			_vm->adsInterpreter()->setHitTTMOp0110();
+		else
+			env._cdsSeqNum++;
 		break;
 	case 0x0220: // STOP CURRENT MUSIC
 		if (seq._executed) // this is a one-shot op
@@ -651,7 +655,12 @@ bool TTMInterpreter::handleOperation(TTMEnviro &env, TTMSeq &seq, uint16 op, byt
 		// TODO: Probably should do this accounting (as well as timeCut and dialogs)
 		// 		 in game frames, not millis.
 		int delayMillis = (int)round(ivals[0] * MS_PER_FRAME);
-		_vm->adsInterpreter()->setScriptDelay(delayMillis);
+		// Slight HACK - if we are running from CDS (Willy Beamish conversation) script,
+		// set that delay, otherwise set ADS interpreter delay.
+		if (env._cdsSeqNum >= 0)
+			env._cdsDelay = delayMillis;
+		else
+			_vm->adsInterpreter()->setScriptDelay(delayMillis);
 		break;
 	}
 	case 0x1030: // SET BRUSH:	id:int [-1:n]
@@ -778,13 +787,13 @@ bool TTMInterpreter::handleOperation(TTMEnviro &env, TTMSeq &seq, uint16 op, byt
 		break;
 	}
 	case 0x3200:
-		env._cdsTarget = findGOTOTarget(env, seq, ivals[0]);
+		env._cdsSeqNum = findGOTOTarget(env, seq, ivals[0]);
 		break;
 	case 0x3300:
-		if (!env._cdsJumped && env._frameOffsets[env._cdsTarget] != env.scr->pos()) {
+		if (!env._cdsJumped && env._frameOffsets[env._cdsSeqNum] != seq._currentFrame) {
 			env._cdsJumped = true;
 			int64 prevPos = env.scr->pos();
-			env.scr->seek(env._frameOffsets[env._cdsTarget]);
+			env.scr->seek(env._frameOffsets[env._cdsSeqNum]);
 			run(env, seq);
 			env.scr->seek(prevPos);
 			env._cdsJumped = false;
@@ -842,7 +851,9 @@ bool TTMInterpreter::handleOperation(TTMEnviro &env, TTMSeq &seq, uint16 op, byt
 	case 0x4200: { // STORE AREA: x,y,w,h:int [0..n]  ; makes this area of foreground persist in the next frames.
 		if (seq._executed) // this is a one-shot op
 			break;
-		const Common::Rect rect(Common::Point(ivals[0], ivals[1]), ivals[2], ivals[3]);
+		Common::Rect rect(Common::Point(ivals[0], ivals[1]), ivals[2], ivals[3]);
+		if (env._cdsSeqNum >= 0)
+			rect.translate(env._xOff, env._yOff);
 		_vm->getStoredAreaBuffer().blitFrom(_vm->_compositionBuffer, rect, rect);
 		break;
 	}
@@ -973,7 +984,8 @@ bool TTMInterpreter::handleOperation(TTMEnviro &env, TTMSeq &seq, uint16 op, byt
 		if (img) {
 			int x = ivals[0];
 			int y = ivals[1];
-			if (_stackDepth > 0) {
+			// Use env offset if we are in gosub *or* running from CDS
+			if (_stackDepth > 0 || env._cdsSeqNum >= 0) {
 				x += env._xOff;
 				y += env._yOff;
 			}
@@ -1122,17 +1134,17 @@ bool TTMInterpreter::handleOperation(TTMEnviro &env, TTMSeq &seq, uint16 op, byt
 		uint16 lo = (uint16)ivals[0];
 		uint32 offset = ((uint32)hi << 16) + lo;
 		debug("TODO: 0xC250 Sync raw sfx?? offset %d", offset);
-		/*
-		if (env._soundRaw->playedOffset() < offset) {
+		/*if (env._soundRaw->playedOffset() < offset) {
 			// Not played to this point yet.
-			env.scr->seek(-6);
+			env.scr->seek(-6, SEEK_CUR);
 			return false;
-		}
-		*/
+		}*/
 	}
 	case 0xf010: { // LOAD SCR:	filename:str
 		if (seq._executed) // this is a one-shot op
 			break;
+		if (env._cdsSeqNum >= 0) // don't run from CDS scripts?
+			break;
 		Image tmp(_vm->getResourceManager(), _vm->getDecompressor());
 		tmp.drawScreen(sval, _vm->getBackgroundBuffer());
 		_vm->_compositionBuffer.blitFrom(_vm->getBackgroundBuffer());
diff --git a/engines/dgds/ttm.h b/engines/dgds/ttm.h
index cf73d26495a..f4a1f321659 100644
--- a/engines/dgds/ttm.h
+++ b/engines/dgds/ttm.h
@@ -41,7 +41,7 @@ class TTMEnviro : public ScriptParserData {
 public:
 	TTMEnviro() : _totalFrames(330), _enviro(0), _creditScrollMeasure(0),
 			_creditScrollYOffset(0), _xOff(0), _yOff(0), _xScroll(0), _yScroll(0),
-			_cdsTarget(0), _cdsJumped(false), ScriptParserData() {
+			_cdsSeqNum(-1), _cdsJumped(false), _cdsDelay(0), ScriptParserData() {
 		ARRAYCLEAR(_scriptPals);
 	}
 
@@ -65,7 +65,8 @@ public:
 	int16 _xScroll;
 	int16 _yScroll;
 	Common::SharedPtr<SoundRaw> _soundRaw;
-	int16 _cdsTarget; // The GOTO target to use in the CDS script (Willy Beamish talkie)
+	int16 _cdsSeqNum; // The GOTO target to use in the CDS script (Willy Beamish talkie)
+	int16 _cdsDelay;
 	bool _cdsJumped;
 };
 


Commit: 217f733e8a90dc44e1e2bd8cf484680c52989211
    https://github.com/scummvm/scummvm/commit/217f733e8a90dc44e1e2bd8cf484680c52989211
Author: Matthew Duggan (mgithub at guarana.org)
Date: 2024-12-16T18:27:39+11:00

Commit Message:
DGDS: Fix some issues in train game from Coverity

Changed paths:
    engines/dgds/minigames/china_train.cpp
    engines/dgds/minigames/china_train.h


diff --git a/engines/dgds/minigames/china_train.cpp b/engines/dgds/minigames/china_train.cpp
index 7489c2f90a6..49425148c5d 100644
--- a/engines/dgds/minigames/china_train.cpp
+++ b/engines/dgds/minigames/china_train.cpp
@@ -35,7 +35,10 @@
 
 namespace Dgds {
 
-/* Not used anywhere.
+/*
+ * Not used anywhere, but these strings are in the EXE and conveniently
+ * tell us what the PlayerAction enum values are.
+ *
 static const char *ACTIONS[] = {
 	"Stand Right",
 	"Walk Right",
@@ -1416,10 +1419,17 @@ void ChinaTrain::checkRegions(TrainPlayer &player) {
 			if (mode == 0 || (mode == 2 && _cabooseTrail == 0)) {
 				player.setAction(kActionJumpRight, true);
 				engine->_soundPlayer->playSFX(134);
-			} else if (mode == 0 && _cabooseTrail != 0) {
-				player.setAction(kActionHeroicJump, true);
-				engine->_soundPlayer->playSFX(134);
 			}
+			//
+			// The original also has this code, but it can never execute because
+			// mode == 0 would have taken the above branch.
+			// There's also no code to handle kActionHeroicJump in doProcess,
+			// so it was probably vestigal.
+			//
+			// } else if (mode == 0 && _cabooseTrail != 0) {
+			//    player.setAction(kActionHeroicJump, true);
+			//    engine->_soundPlayer->playSFX(134);
+			// }
 		} else if (player._action == kActionWalkLeft) {
 			if ((mode == 1 || mode == 3) && (_players._tong._intent != 5 || _currentCar != 3 || &player == &_players._tong)) {
 				player.setAction(kActionJumpLeft, true);
diff --git a/engines/dgds/minigames/china_train.h b/engines/dgds/minigames/china_train.h
index d9505c150fd..23ac5c8ea8c 100644
--- a/engines/dgds/minigames/china_train.h
+++ b/engines/dgds/minigames/china_train.h
@@ -82,6 +82,7 @@ struct PlayerData {
 };
 
 struct TunnelData {
+	TunnelData() : _start(0), _end(0) {}
 	int32 _start;
 	int32 _end;
 };
@@ -94,7 +95,7 @@ public:
 	bool isBlocking() const { return _action == kActionBlock || _action == kActionBlockUp; }
 	bool isDucking() const { return _action == kActionDuckRight || _action == kActionDuckLeft; }
 	bool isFalling() const { return _action == kActionFallRight || _action == kActionFallLeft; }
-	bool isJumping() const { return _action == kActionJumpRight || _action == kActionJumpRight; }
+	bool isJumping() const { return _action == kActionJumpRight || _action == kActionJumpLeft; }
 	bool isStaggering() const { return _action == kActionStagger; }
 	bool isStanding() const { return _action == kActionStandRight || _action == kActionStandLeft; }
 	bool isWalking() const { return _action == kActionWalkLeft || _action == kActionWalkRight; }




More information about the Scummvm-git-logs mailing list