/* ScummVM - Graphic Adventure Engine
 *
 * ScummVM is the legal property of its developers, whose names
 * are too numerous to list here. Please refer to the COPYRIGHT
 * file distributed with this source distribution.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include "common/debug.h"
#include "common/endian.h"
#include "common/file.h"
#include "common/rect.h"
#include "common/system.h"
#include "common/util.h"

#include "dgds/dgds.h"
#include "dgds/includes.h"
#include "dgds/resource.h"
#include "dgds/scene.h"
#include "dgds/ads.h"
#include "dgds/globals.h"
#include "dgds/inventory.h"
#include "dgds/debug_util.h"
#include "dgds/game_palettes.h"

namespace Dgds {


//
// The number of frames between mouse down and the action changing:
//           short     long
// L button: use   ->  pick up
// R button: look  ->  target
//
static const int MOUSE_DOWN_TIMEOUT = 3;

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 += DebugUtil::dumpStructList(indent, "enableConditions", enableConditions);
	str += DebugUtil::dumpStructList(indent, "onLookOps", onLookOps);
	str += DebugUtil::dumpStructList(indent, "onPickUpOps", onPickUpOps);
	str += DebugUtil::dumpStructList(indent, "onUseOps", onUseOps);
	str += "\n";
	str += indent + ">";
	return str;
}


Common::String GameItem::dump(const Common::String &indent) const {
	Common::String super = HotArea::dump(indent + "  ");

	Common::String str = Common::String::format(
			"%sGameItem<\n%s\n%saltCursor %d icon %d sceneNum %d flags 0x%x quality %d",
			indent.c_str(), super.c_str(), indent.c_str(), _altCursor,
			_iconNum, _inSceneNum, _flags, _quality);
	str += DebugUtil::dumpStructList(indent, "onDragFinishedOps", onDragFinishedOps);
	str += DebugUtil::dumpStructList(indent, "onBothButtonsOps", onBothButtonsOps);
	str += "\n";
	str += indent + ">";
	return str;
}


Common::String MouseCursor::dump(const Common::String &indent) const {
	return Common::String::format("%sMouseCursor<%d %d>", indent.c_str(), _hot.x, _hot.y);
}


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 += DebugUtil::dumpStructList(indent, "opList", opList);
	str += "\n";
	str += indent + ">";
	return str;
}


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 += DebugUtil::dumpStructList(indent, "conditionList", conditionList);
	str += DebugUtil::dumpStructList(indent, "opList", sceneOpList);
	str += "\n";
	str += indent + ">";
	return str;
}


Common::String PerSceneGlobal::dump(const Common::String &indent) const {
	return Common::String::format("%sPerSceneGlobal<num %d scene %d val %d>", indent.c_str(), _num, _sceneNo, _val);
}

// //////////////////////////////////// //

//
// Check that a list length seems "sensible" so we can crash with
// a nice error message instead of crash trying to allocate a
// massive list.
//
static void _checkListNotTooLong(uint16 len, const char *list_type) {
	if (len > 1000)
		error("Too many %s in list (%d), scene data is likely corrupt.", list_type, len);
}

Scene::Scene() : _magic(0) {
}

bool Scene::isVersionOver(const char *version) const {
	assert(!_version.empty());
	return strncmp(_version.c_str(), version, _version.size()) > 0;
}

bool Scene::isVersionUnder(const char *version) const {
	assert(!_version.empty());
	return strncmp(_version.c_str(), version, _version.size()) < 0;
}


bool Scene::readConditionList(Common::SeekableReadStream *s, Common::Array<SceneConditions> &list) const {
	uint16 num = s->readUint16LE();
	_checkListNotTooLong(num, "scene conditions");
	for (uint16 i = 0; i < num; i++) {
		uint16 cnum = s->readUint16LE();
		SceneCondition cond = static_cast<SceneCondition>(s->readUint16LE());
		int16 val = s->readSint16LE();
		list.push_back(SceneConditions(cnum, cond, val));
	}
	return !s->err();
}


bool Scene::readHotArea(Common::SeekableReadStream *s, HotArea &dst) const {
	dst._rect.x = s->readUint16LE();
	dst._rect.y = s->readUint16LE();
	dst._rect.width = s->readUint16LE();
	dst._rect.height = s->readUint16LE();
	dst._num = s->readUint16LE();
	dst._cursorNum = s->readUint16LE();
	if (isVersionOver(" 1.217"))
		dst._cursorNum2 = s->readUint16LE();
	else
		dst._cursorNum2 = 0;

	if (isVersionOver(" 1.218")) {
		dst._objInteractionRectNum = s->readUint16LE();
		if (dst._objInteractionRectNum) {
			dst._rect = DgdsRect();
		}
	} else {
		dst._objInteractionRectNum = 0;
	}
	readConditionList(s, dst.enableConditions);
	readOpList(s, dst.onLookOps);
	readOpList(s, dst.onPickUpOps);
	readOpList(s, dst.onUseOps);
	return !s->err();
}


bool Scene::readHotAreaList(Common::SeekableReadStream *s, Common::List<HotArea> &list) const {
	uint16 num = s->readUint16LE();
	_checkListNotTooLong(num, "hot areas");
	for (uint16 i = 0; i < num; i++) {
		HotArea dst;
		readHotArea(s, dst);
		list.push_back(dst);
	}
	return !s->err();
}


bool Scene::readGameItemList(Common::SeekableReadStream *s, Common::Array<GameItem> &list) const {
	uint16 num = s->readUint16LE();
	_checkListNotTooLong(num, "game items");
	list.resize(num);

	for (GameItem &dst : list) {
		readHotArea(s, dst);
	}
	for (GameItem &dst : list) {
		dst._iconNum = s->readUint16LE();
		dst._inSceneNum = s->readUint16LE();
		dst._quality = s->readUint16LE();
		if (!isVersionUnder(" 1.211"))
			dst._flags = s->readUint16LE() & 0xfffe;
		if (!isVersionUnder(" 1.204")) {
			dst._altCursor = s->readUint16LE();
			readOpList(s, dst.onDragFinishedOps);
			readOpList(s, dst.onBothButtonsOps);
		}
	}
	return !s->err();
}


bool Scene::readMouseHotspotList(Common::SeekableReadStream *s, Common::Array<MouseCursor> &list) const {
	uint16 num = s->readUint16LE();
	uint16 hotX, hotY;
	_checkListNotTooLong(num, "mouse hotspots");

	for (uint16 i = 0; i < num; i++) {
		hotX = s->readUint16LE();
		hotY = s->readUint16LE();
		list.push_back(MouseCursor(hotX, hotY));
	}
	return !s->err();
}


bool Scene::readObjInteractionList(Common::SeekableReadStream *s, Common::Array<ObjectInteraction> &list) const {
	uint16 num = s->readUint16LE();
	_checkListNotTooLong(num, "interactions");

	for (uint16 i = 0; i < num; i++) {
		uint16 dropped, target;
		if (!isVersionOver(" 1.205")) {
			target = s->readUint16LE();
			dropped = s->readUint16LE();
			target += s->readUint16LE();
		} else {
			dropped = s->readUint16LE();
			target = s->readUint16LE();
		}
		list.push_back(ObjectInteraction(dropped, target));
		readOpList(s, list.back().opList);
	}
	return !s->err();
}


bool Scene::readOpList(Common::SeekableReadStream *s, Common::Array<SceneOp> &list) const {
	uint16 nitems = s->readUint16LE();
	_checkListNotTooLong(nitems, "scene ops");
	list.resize(nitems);
	for (SceneOp &dst : list) {
		readConditionList(s, dst._conditionList);
		dst._opCode = static_cast<SceneOpCode>(s->readUint16LE());
		if ((dst._opCode & ~kSceneOpHasConditionalOpsFlag) > kSceneOpMaxCode || dst._opCode == kSceneOpNone)
			error("Unexpected scene opcode %d", (int)dst._opCode);
		uint16 nvals = s->readUint16LE();
		_checkListNotTooLong(nvals, "scene op args");
		for (uint16 i = 0; i < nvals / 2; i++) {
			dst._args.push_back(s->readUint16LE());
		}
	}

	return !s->err();
}


bool Scene::readDialogList(Common::SeekableReadStream *s, Common::List<Dialog> &list, int16 filenum /* = 0 */) const {
	// Some data on this format here https://www.oldgamesitalia.net/forum/index.php?showtopic=24055&st=25&p=359214&#entry359214

	uint16 nitems = s->readUint16LE();
	_checkListNotTooLong(nitems, "dialogs");

	for (uint i = 0; i < nitems; i++) {
		Dialog dst;
		dst._num = s->readUint16LE();
		dst._fileNum = filenum;
		dst._rect.x = s->readUint16LE();
		dst._rect.y = s->readUint16LE();
		dst._rect.width = s->readUint16LE();
		dst._rect.height = s->readUint16LE();
		dst._bgColor = s->readUint16LE();
		dst._fontColor = s->readUint16LE(); // 0 = black, 0xf = white
		if (isVersionUnder(" 1.209")) {
			dst._selectionBgCol = dst._bgColor;
			dst._selectonFontCol = dst._fontColor;
		} else {
			dst._selectionBgCol = s->readUint16LE();
			dst._selectonFontCol = s->readUint16LE();
		}
		dst._fontSize = s->readUint16LE(); // 01 = 8x8, 02 = 6x6, 03 = 4x5
		if (isVersionUnder(" 1.210")) {
			dst._flags = static_cast<DialogFlags>(s->readUint16LE());
		} else {
			// Game reads a 32 bit int but then truncates anyway..
			// probably never used the full thing in SDS files
			// as most higher bits are render state.
			dst._flags = static_cast<DialogFlags>(s->readUint32LE() & 0xffff);
		}

		dst._frameType = static_cast<DialogFrameType>(s->readUint16LE());
		dst._time = s->readUint16LE();
		if (isVersionOver(" 1.215")) {
			dst._nextDialogFileNum = s->readUint16LE();
		} else {
			dst._nextDialogFileNum = 0;
		}
		if (isVersionOver(" 1.207")) {
			dst._nextDialogDlgNum = s->readUint16LE();
		}

		if (isVersionOver(" 1.216")) {
			dst._talkDataNum = s->readUint16LE();
			dst._talkDataHeadNum = s->readUint16LE();
		}

		uint16 nbytes = s->readUint16LE();
		if (nbytes > 0) {
			dst._str = s->readString('\0', nbytes);
		} else {
			dst._str.clear();
		}
		readDialogActionList(s, dst._action);

		if (isVersionUnder(" 1.209") && !dst._action.empty()) {
			if (dst._fontColor == 0)
				dst._selectonFontCol = 4;
			else if (dst._fontColor == 0xff)
				dst._fontColor = 7;
			else
				dst._fontColor = dst._fontColor ^ 8;
		}
		list.push_back(dst);
	}

	return !s->err();
}


bool Scene::readTriggerList(Common::SeekableReadStream *s, Common::Array<SceneTrigger> &list) const {
	uint16 num = s->readUint16LE();
	_checkListNotTooLong(num, "triggers");
	for (uint16 i = 0; i < num; i++) {
		list.push_back(SceneTrigger(s->readUint16LE()));
		if (isVersionOver(" 1.219"))
			list.back()._timesToCheckBeforeRunning = s->readUint16LE();
		readConditionList(s, list.back().conditionList);
		readOpList(s, list.back().sceneOpList);
	}

	return !s->err();
}

bool Scene::readConditionalSceneOpList(Common::SeekableReadStream *s, Common::Array<ConditionalSceneOp> &list) const {
	uint16 num = s->readUint16LE();
	_checkListNotTooLong(num, "conditional scene ops");
	list.resize(num);

	for (ConditionalSceneOp &dst : list) {
		dst._opCode = static_cast<SceneOpCode>(s->readUint16LE());
		if (dst._opCode > kSceneOpMaxCode || dst._opCode == kSceneOpNone)
			error("Unexpected scene opcode %d", (int)dst._opCode);
		readConditionList(s, dst._conditionList);
		readOpList(s, dst._opList);
	}
	return !s->err();
}


bool Scene::readDialogActionList(Common::SeekableReadStream *s, Common::Array<DialogAction> &list) const {
	uint16 num = s->readUint16LE();
	_checkListNotTooLong(num, "dialog actions");
	list.resize(num);

	// The original initializes a field in the first entry to 1 here, but it seems
	// only used for memory management so we don't need it?
	// if (!list.empty())
	//	list[0].val = 1;

	for (uint i = 0; i < list.size(); i++) {
		list[i].num = i;
		list[i].strStart = s->readUint16LE();
		list[i].strEnd = s->readUint16LE();
		readOpList(s, list[i].sceneOpList);
	}

	return !s->err();
}


void Scene::setItemAttrOp(const Common::Array<uint16> &args) {
	if (args.size() < 3)
		error("Expect 3 args for item attr opcode.");

	DgdsEngine *engine = DgdsEngine::getInstance();
	for (auto &item : engine->getGDSScene()->getGameItems()) {
		if (item._num != args[0])
			continue;

		if (args[1] != 0xffff) {
			//bool doDraw = item._inSceneNum != args[1] && engine->getScene()->getNum() == args[1];
			item._inSceneNum = args[1];
		}

		if (args[2])
			item._quality = args[2];

		break;
	}
}

void Scene::setDragItemOp(const Common::Array<uint16> &args) {
	DgdsEngine *engine = DgdsEngine::getInstance();

	for (auto &item : engine->getGDSScene()->getGameItems()) {
		if (item._num != args[0])
			continue;

		bool inScene = (item._inSceneNum == engine->getScene()->getNum());
		engine->getScene()->setDragItem(&item);
		if (item._inSceneNum == 2)
			item._flags |= kItemStateWasInInv;
		if (!inScene)
			item._inSceneNum = engine->getScene()->getNum(); // else do some redraw??

		Common::Point lastMouse = engine->getLastMouse();
		item._rect.x = lastMouse.x;
		item._rect.y = lastMouse.y;
		engine->setMouseCursor(item._iconNum);
	}
}

void Scene::segmentStateOps(const Common::Array<uint16> &args) {
	ADSInterpreter *interp = DgdsEngine::getInstance()->adsInterpreter();

	for (uint i = 0; i < args.size(); i += 2) {
		uint16 subop = args[i];
		uint16 arg = args[i + 1];
		if (!subop && !arg)
			return;
		switch (subop) {
		case 1: // Restart
			interp->segmentOrState(arg, 3);
			break;
		case 2: // Start
			interp->segmentOrState(arg, 4);
			break;
		case 3: // Stop
			interp->segmentSetState(arg, 6);
			break;
		case 4: // Pause
			interp->segmentSetState(arg, 5);
			break;
		case 9:
			warning("TODO: Apply segment state 3 to all loaded ADS texts");
			interp->segmentOrState(arg, 3);
			break;
		case 10:
			warning("TODO: Apply segment state 4 to all loaded ADS texts");
			interp->segmentOrState(arg, 4);
			break;
		case 11:
			warning("TODO: Apply segment state 6 to all loaded ADS texts");
			interp->segmentSetState(arg, 6);
			break;
		case 12:
			warning("TODO: Apply segment state 5 to all loaded ADS texts");
			interp->segmentSetState(arg, 5);
			break;
		default:
			error("Unknown scene op 4 sub-opcode %d", subop);
		}
	}
}


//
// 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
// we have to finish executing the list if the scene changed into inventory -
// which could have invalidated the op list pointer.  We *don't* finish executing
// if any other scene change happens.
//
// Because scene change can also invalidate the `this` pointer, this is static
// and the runOp functions fetch the scene through the engine.
//
/*static*/
bool Scene::runOps(const Common::Array<SceneOp> ops, int16 addMinuites /* = 0 */) {
	DgdsEngine *engine = DgdsEngine::getInstance();
	bool sceneChanged = false;
	int16 startSceneNum = engine->getScene()->getNum();
	for (const SceneOp &op : ops) {
		if (!SceneConditions::check(op._conditionList))
			continue;
		debug(10, "Exec %s", op.dump("").c_str());
		if (addMinuites) {
			engine->getClock().addGameTime(addMinuites);
			addMinuites = 0;
		}

		sceneChanged = op.runOp();

		if (sceneChanged)
			break;
	}

	//
	// The definition of "scene changed" returned by this function is slightly different -
	// for the purpose of continuing to run ops above, we ignore changes to scene 2 (the
	// inventory), but for the purpose of telling the caller, any change means they
	// need to stop as pointers are no longer valid.
	//
	int16 endSceneNum = engine->getScene()->getNum();
	return (startSceneNum == endSceneNum) && !sceneChanged;
}


bool SDSScene::_dlgWithFlagLo8IsClosing = false;
DialogFlags SDSScene::_sceneDialogFlags = kDlgFlagNone;

SDSScene::SDSScene() : _num(-1), _dragItem(nullptr), _shouldClearDlg(false), _ignoreMouseUp(false),
_field6_0x14(0), _rbuttonDown(false), _lbuttonDown(false), _lookMode(0), _lbuttonDownWithDrag(false),
_mouseDownArea(nullptr), _mouseDownCounter(0) {
}

bool SDSScene::load(const Common::String &filename, ResourceManager *resourceManager, Decompressor *decompressor) {
	Common::SeekableReadStream *sceneFile = resourceManager->getResource(filename);
	if (!sceneFile)
		error("Scene file %s not found", filename.c_str());

	DgdsChunkReader chunk(sceneFile);

	bool result = false;

	while (chunk.readNextHeader(EX_SDS, filename)) {
		if (chunk.isContainer()) {
			continue;
		}

		chunk.readContent(decompressor);
		Common::SeekableReadStream *stream = chunk.getContent();

		if (chunk.isSection(ID_SDS)) {
			result = parse(stream);
		}
	}

	delete sceneFile;

	return result;
}


bool SDSScene::parse(Common::SeekableReadStream *stream) {
	_magic = stream->readUint32LE();
	_version = stream->readString();
	//if (isVersionOver(" 1.211")) { // Dragon
	//if (isVersionOver(" 1.216")) { // HoC
	if (isVersionOver(" 1.224")) { // Beamish
		error("Unsupported scene version '%s'", _version.c_str());
	}
	_num = stream->readUint16LE();
	readOpList(stream, _enterSceneOps);
	readOpList(stream, _leaveSceneOps);
	if (isVersionOver(" 1.206")) {
		readOpList(stream, _preTickOps);
	}
	readOpList(stream, _postTickOps);
	_field6_0x14 = stream->readUint16LE();
	_adsFile = stream->readString();
	readHotAreaList(stream, _hotAreaList);
	readObjInteractionList(stream, _objInteractions1);
	if (isVersionOver(" 1.205")) {
		readObjInteractionList(stream, _objInteractions2);
	}
	if (isVersionUnder(" 1.214")) {
		readDialogList(stream, _dialogs);
	}
	if (isVersionOver(" 1.203")) {
		readTriggerList(stream, _triggers);
	}
	if (isVersionOver(" 1.223")) {
		readConditionalSceneOpList(stream, _conditionalOps);
	}

	return !stream->err();
}

void SDSScene::unload() {
	_num = 0;
	_lookMode = 0;
	_enterSceneOps.clear();
	_leaveSceneOps.clear();
	_preTickOps.clear();
	_postTickOps.clear();
	_field6_0x14 = 0;
	_adsFile.clear();
	_hotAreaList.clear();
	_objInteractions1.clear();
	_objInteractions2.clear();
	_dialogs.clear();
	_triggers.clear();
	_talkData.clear();
	_dynamicRects.clear();
	_conversation.unloadData();
	_conditionalOps.clear();
	_sceneDialogFlags = kDlgFlagNone;
	_mouseDownArea = nullptr;
	_mouseDownCounter = 0;
	_rbuttonDown = false;
	_lbuttonDown = false;
}


Common::String SDSScene::dump(const Common::String &indent) const {
	Common::String str = Common::String::format("%sSDSScene<ver %s num %d %d ads %s", indent.c_str(), _version.c_str(), _num, _field6_0x14, _adsFile.c_str());
	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 += DebugUtil::dumpStructList(indent, "conditionalOps", _conditionalOps);

	str += "\n";
	str += indent + ">";
	return str;
}


void SDSScene::enableTrigger(uint16 sceneNum, uint16 num, bool enable /* = true */) {
	if (sceneNum && sceneNum != _num)
		return;

	for (auto &trigger : _triggers) {
		if (trigger.getNum() == num) {
			trigger._enabled = enable;
			if (enable)
				trigger._checksUntilRun = trigger._timesToCheckBeforeRunning;
			return;
		}
	}

	warning("enableTrigger: Trigger %d not found", num);
}

bool SDSScene::isTriggerEnabled(uint16 num) {
	for (auto &trigger : _triggers) {
		if (trigger.getNum() == num) {
			return trigger._enabled;
		}
	}

	warning("isTriggerEnabled: Trigger %d not found", num);
	return false;
}

void SDSScene::checkTriggers() {
	for (SceneTrigger &trigger : _triggers) {
		if (!trigger._enabled)
			continue;

		if (trigger._checksUntilRun) {
			trigger._checksUntilRun--;
			continue;
		}

		if (!SceneConditions::check(trigger.conditionList))
			continue;

		trigger._enabled = false;
		bool keepGoing = runOps(trigger.sceneOpList);

		// If the scene changed, the list is no longer valid. Abort!
		if (!keepGoing)
			return;
	}
}


void SDSScene::loadDialogData(uint16 fileNum) {
	if (fileNum == 0)
		return;

	for (auto &dlg: _dialogs)
		if (dlg._fileNum == fileNum)
			// already loaded
			return;

	const Common::String filename = Common::String::format("D%d.DDS", fileNum);
	DgdsEngine *engine = DgdsEngine::getInstance();
	ResourceManager *resourceManager = engine->getResourceManager();
	Common::SeekableReadStream *dlgFile = resourceManager->getResource(filename);
	if (!dlgFile) {
		//
		// This happens for example if debug mode clicks have been enabled in
		// Willy Beamish, as the debug dialogs were not included in the retail
		// version.
		//
		warning("Dialog file %s not found", filename.c_str());
		return;
	}

	DgdsChunkReader chunk(dlgFile);
	Decompressor *decompressor = engine->getDecompressor();

	bool result = false;

	uint prevSize = _dialogs.size();

	Common::String fileVersion;
	Common::String fileId;
	while (chunk.readNextHeader(EX_DDS, filename)) {
		if (chunk.isContainer()) {
			continue;
		}

		chunk.readContent(decompressor);
		Common::SeekableReadStream *stream = chunk.getContent();

		if (chunk.isSection(ID_DDS)) {
			uint32 magic = stream->readUint32LE();
			if (magic != _magic)
				error("Dialog file magic mismatch %08x vs scene %08x", magic, _magic);
			fileVersion = stream->readString();
			fileId = stream->readString();
			// slight hack, set file version while loading
			Common::String oldVer = _version;
			_version = fileVersion;
			result = readDialogList(stream, _dialogs, fileNum);
			_version = oldVer;
		}
	}

	delete dlgFile;

	if (_dialogs.size() != prevSize) {
		debug(10, "Read %d dialogs from DDS %s (ver %s id '%s'):", _dialogs.size() - prevSize,
			filename.c_str(), fileVersion.c_str(), fileId.c_str());
		for (const auto &dlg: _dialogs)
			if (dlg._fileNum == fileNum)
				debug(10, "%s", dlg.dump("").c_str());
	}

	if (!result)
		return;

	for (auto &dlg : _dialogs) {
		if (dlg._nextDialogDlgNum && !dlg._nextDialogFileNum) {
			dlg._nextDialogFileNum = fileNum;
		}
	}
}

void SDSScene::freeDialogData(uint16 fileNum) {
	if (!fileNum)
		return;

	for (Common::List<Dialog>::iterator iter = _dialogs.begin(); iter != _dialogs.end(); iter++) {
		if (iter->_fileNum == fileNum)
			iter = _dialogs.erase(iter);
	}
}

bool SDSScene::readTalkData(Common::SeekableReadStream *s, TalkData &dst) {
	dst._bmpFile = s->readString();

	uint16 nvals = s->readUint16LE();
	_checkListNotTooLong(nvals, "talk data");
	dst._heads.resize(nvals);
	for (auto &h : dst._heads) {
		h._num = s->readUint16LE();
		h._drawType = s->readUint16LE();
		h._drawCol = s->readUint16LE();
		h._rect.x = s->readUint16LE();
		h._rect.y = s->readUint16LE();
		h._rect.width = s->readUint16LE();
		h._rect.height = s->readUint16LE();
		if (isVersionOver(" 1.220")) {
			h._bmpFile = s->readString();
			if (!h._bmpFile.empty()) {
				DgdsEngine *engine = DgdsEngine::getInstance();
				ResourceManager *resMan = engine->getResourceManager();
				if (resMan->hasResource(h._bmpFile)) {
					h._shape.reset(new Image(resMan, engine->getDecompressor()));
					h._shape->loadBitmap(h._bmpFile);
				} else {
					// This is the default situation in Willy Beamish CD
					debug("Couldn't load talkdata %d head %d BMP: %s", dst._num, h._num, h._bmpFile.c_str());
				}
			}
		}
		uint16 nsub = s->readUint16LE();
		_checkListNotTooLong(nsub, "talk head frames");
		h._headFrames.resize(nsub);
		for (auto &sub : h._headFrames) {
			sub._frameNo = s->readUint16LE();
			sub._xoff = s->readSint16LE();
			sub._yoff = s->readSint16LE();
			if (isVersionOver(" 1.221")) {
				sub._flipFlags = s->readUint16LE();
			}
		}
	}

	return true;
}

bool SDSScene::loadTalkData(uint16 num) {
	if (!num)
		return false;

	for (auto &talk : _talkData) {
		if (talk._num == num)
			return true;
	}

	const Common::String filename = Common::String::format("T%d.TDS", num);
	DgdsEngine *engine = DgdsEngine::getInstance();
	ResourceManager *resourceManager = engine->getResourceManager();
	Common::SeekableReadStream *dlgFile = resourceManager->getResource(filename);
	if (!dlgFile)
		error("Talk file %s not found", filename.c_str());

	DgdsChunkReader chunk(dlgFile);
	Decompressor *decompressor = engine->getDecompressor();

	bool result = false;

	while (chunk.readNextHeader(EX_TDS, filename)) {
		if (chunk.isContainer()) {
			continue;
		}

		chunk.readContent(decompressor);
		Common::SeekableReadStream *stream = chunk.getContent();

		if (chunk.isSection(ID_THD)) {
			uint32 magic = stream->readUint32LE();
			if (magic != _magic)
				error("Talk file magic mismatch %08x vs scene %08x", magic, _magic);
			Common::String fileVersion = stream->readString();
			Common::String fileId = stream->readString();
			// slight hack, set file version while loading
			Common::String oldVer = _version;
			_version = fileVersion;
			_talkData.insert_at(0, TalkData());
			result = readTalkData(stream, _talkData.front());
			_talkData.front()._num = num;
			_version = oldVer;

			const Common::String &bmpFile = _talkData.front()._bmpFile;
			if (!bmpFile.empty()) {
				if (resourceManager->hasResource(bmpFile)) {
					Image *img = new Image(resourceManager, decompressor);
					img->loadBitmap(bmpFile);
					_talkData.front()._shape.reset(img);
				} else {
					warning("Couldn't load talkdata %d head BMP: %s", num, bmpFile.c_str());
				}
			}
		}
	}

	delete dlgFile;

	return result;
}


bool SDSScene::freeTalkData(uint16 num) {
	bool result = false;
	for (int i = 0; i < (int)_talkData.size(); i++) {
		if (_talkData[i]._num == num) {
			_talkData.remove_at(i);
			i--;
			result = true;
		}
	}
	return result;
}

void SDSScene::clearVisibleTalkers() {
	for (auto &data : _talkData) {
		data.clearVisibleHeads();
	}
}

void SDSScene::drawAndUpdateHeads(Graphics::ManagedSurface &dst) {
	for (auto &tds : _talkData) {
		tds.drawAndUpdateVisibleHeads(dst);
	}

	if (_conversation.isForDlg(getVisibleDialog())) {
		_conversation.runScript();
	}
}

bool SDSScene::hasVisibleHead() const {
	for (const auto &tds : _talkData) {
		if (tds.hasVisibleHead())
			return true;
	}
	return false;
}


bool SDSScene::loadTalkDataAndSetFlags(uint16 talknum, uint16 headnum) {
	clearVisibleTalkers();

	_conversation._drawRect = DgdsRect();
	if (loadTalkData(talknum)) {
		for (auto &data : _talkData) {
			if (data._num != talknum)
				continue;

			for (auto &head : data._heads) {
				if (head._num != headnum)
					continue;

				_conversation._drawRect = head._rect;
				head._flags = static_cast<HeadFlags>(head._flags & ~(kHeadFlagFinished | kHeadFlag10));
				head._flags = static_cast<HeadFlags>(head._flags | (kHeadFlag8 | kHeadFlagVisible | kHeadFlagOpening));
				break;
			}
			break;
		}
		return true;
	}
	return false;
}


static const uint16 TIRED_DLG_ID = 7777;

void SDSScene::addAndShowTiredDialog() {
	bool haveTiredDlg = false;
	for (auto &d : _dialogs) {
		if (d._num == TIRED_DLG_ID) {
			haveTiredDlg = true;
			break;
		}
	}
	if (!haveTiredDlg) {
		Dialog dlg;
		dlg._num = TIRED_DLG_ID;
		dlg._rect = DgdsRect(4, 18, 208, 91);
		dlg._bgColor = 15;
		dlg._fontColor = 0;
		dlg._selectionBgCol = 15;
		dlg._selectonFontCol = 0;
		dlg._flags = static_cast<DialogFlags>(kDlgFlagLo8 | kDlgFlagLeftJust | kDlgFlagFlatBg);
		dlg._frameType = kDlgFrameThought;
		dlg._time = 420;
		if (DgdsEngine::getInstance()->getGameLang() == Common::EN_ANY) {
			dlg._str = "Boy, am I tired.  Better get some sleep in about an hour.";
		} else if (DgdsEngine::getInstance()->getGameLang() == Common::DE_DEU) {
			dlg._str = "Mensch, bin ich m\x81""de!  Am Besten gehe ich bald mal ins Bett.";
		} else {
			error("Unsupported language %d", DgdsEngine::getInstance()->getGameLang());
		}

		_dialogs.push_back(dlg);
	}
	showDialog(0, TIRED_DLG_ID);
}


void SDSScene::showDialog(uint16 fileNum, uint16 dlgNum) {
	// TODO: In Willy Beamish, if the inventory button is visible here then
	// it should be hidden and a flag set to re-enabled it once the dialog
	// is closed.  Other games leave it visible.

	if (fileNum)
		loadDialogData(fileNum);

	for (auto &dialog : _dialogs) {
		if (dialog._num == dlgNum && fileNum == dialog._fileNum) {
			dialog.clearFlag(kDlgFlagHiFinished);
			dialog.clearFlag(kDlgFlagRedrawSelectedActionChanged);
			dialog.clearFlag(kDlgFlagHi10);
			//dialog.clearFlag(kDlgFlagHi20);
			dialog.clearFlag(kDlgFlagHi40);
			dialog.setFlag(kDlgFlagHi20);
			dialog.setFlag(kDlgFlagVisible);
			dialog.setFlag(kDlgFlagOpening);

			// For beamish
			bool haveHeadData = false;
			if (dialog._talkDataHeadNum) {
				haveHeadData = loadTalkDataAndSetFlags(dialog._talkDataNum, dialog._talkDataHeadNum);
			}

			_conversation.loadData(fileNum, dlgNum, -1, haveHeadData);

			// hide time gets set the first time it's drawn.
			if (_dlgWithFlagLo8IsClosing && dialog.hasFlag(kDlgFlagLo8)) {
				_sceneDialogFlags = static_cast<DialogFlags>(_sceneDialogFlags | kDlgFlagLo8 | kDlgFlagVisible);
			}
			if (_dlgWithFlagLo8IsClosing) {
				// TODO: call some function (FUN_1f1a_4205) here.
			}
		}
	}
}

bool SDSScene::checkDialogActive() {
	uint32 timeNow = DgdsEngine::getInstance()->getThisFrameMs();
	bool retval = false;

	_sceneDialogFlags = kDlgFlagNone;

	bool clearDlgFlag = _shouldClearDlg; // ((g_gameStateFlag_41f6 | UINT_39e5_41f8) & 6) != 0); ??
	_shouldClearDlg = false;

	for (auto &dlg : _dialogs) {
		if (!dlg.hasFlag(kDlgFlagVisible))
			continue;

		if (!dlg._state)
			dlg._state.reset(new DialogState());

		// FIXME: double-check this logic.
		// Mark finished if we are manually clearing *or* the timer has expired.
		bool finished = false;
		if (clearDlgFlag || (dlg._state->_hideTime && timeNow >= dlg._state->_hideTime)) {
			finished = true;
		}

		bool no_options = false;
		if ((dlg._state->_hideTime == 0) && dlg._action.size() < 2)
			no_options = true;

		// If voice acting in Willy Beamish is finished, clear the dialog
		// unless we are waiting for a choice.
		if (dlg._action.size() < 2 && (_conversation.isForDlg(&dlg) && _conversation.isFinished())) {
			finished = true;
			_conversation.clear();
		}

		if ((!finished && !no_options) || dlg.hasFlag(kDlgFlagHi20) || dlg.hasFlag(kDlgFlagHi40)) {
			if (!finished && dlg._action.size() > 1 && !dlg.hasFlag(kDlgFlagHiFinished)) {
				DialogAction *action = dlg.pickAction(false, clearDlgFlag);
				if (dlg._state->_selectedAction != action) {
					dlg._state->_selectedAction = action;
					dlg.clearFlag(kDlgFlagHi10);
					dlg.setFlag(kDlgFlagRedrawSelectedActionChanged);
				}
			}
		} else {
			// this dialog is finished - call the ops and maybe show the next one
			_dlgWithFlagLo8IsClosing = dlg.hasFlag(kDlgFlagLo8);

			// For Willy Beamish
			bool haveHeadData = false;
			if (dlg._talkDataNum) {
				haveHeadData = freeTalkData(dlg._talkDataNum);
			}

			DialogAction *action = dlg.pickAction(true, clearDlgFlag);
			if (action || dlg._action.empty()) {
				dlg.setFlag(kDlgFlagHiFinished);
				if (action) {
					// Play the response voice acting script.
					_conversation.loadData(dlg._fileNum, dlg._num, action->num, haveHeadData);
					_conversation.runScript();

					// Take a copy of the dialog because the actions might change the scene
					Dialog dlgCopy = dlg;
					if (dlgCopy._state)
						dlgCopy._state->_selectedAction = nullptr;
					debug(1, "Dialog %d closing: run action (%d ops)", dlg._num, action->sceneOpList.size());
					if (!runOps(action->sceneOpList)) {
						// HACK: the scene changed, but we haven't yet drawn the foreground for the
						// dialog, this is our last chance so do it now.  The game does it in a
						// different way that relies on delayed disposal of the dialog data.
						if (dlgCopy.hasFlag(kDlgFlagVisible) && !dlgCopy.hasFlag(kDlgFlagOpening)) {
							DgdsEngine *engine = DgdsEngine::getInstance();
							dlgCopy.draw(&engine->_compositionBuffer, kDlgDrawFindSelectionPointXY);
							dlgCopy.draw(&engine->_compositionBuffer, kDlgDrawFindSelectionTxtOffset);
							dlgCopy.draw(&engine->_compositionBuffer, kDlgDrawStageForeground);
						}
						_dlgWithFlagLo8IsClosing = false;
						return true;
					}
				}
			}

			if (dlg._nextDialogDlgNum) {
				dlg.setFlag(kDlgFlagHiFinished);
				showDialog(dlg._nextDialogFileNum, dlg._nextDialogDlgNum);
			} else {
				// No next dialog .. clear CDS data?
				//_conversation.unloadData();
			}
		}
		if (dlg.hasFlag(kDlgFlagVisible)) {
			_sceneDialogFlags = static_cast<DialogFlags>(_sceneDialogFlags | kDlgFlagVisible);
		}
	}
	return retval;
}

void SDSScene::drawActiveDialogBgs(Graphics::ManagedSurface *dst) {
	for (auto &dlg : _dialogs) {
		if (dlg.hasFlag(kDlgFlagVisible) && !dlg.hasFlag(kDlgFlagOpening)) {
			dlg.draw(dst, kDlgDrawStageBackground);
			// FIXME: Original clears Hi20 and sets Hi40 here, but with our
			// call sequence that means the time never works right in
			// drawAndUpdateDialogs??
			//dlg.clearFlag(kDlgFlagHi20);
			//dlg.setFlag(kDlgFlagHi40);
		}
	}
}

bool SDSScene::checkForClearedDialogs() {
	bool result = false;
	bool have8 = false;
	for (auto &dlg : _dialogs) {
		if (!dlg.hasFlag(kDlgFlagHiFinished)) {
			if (dlg.hasFlag(kDlgFlagLo8))
				have8 = true;
		} else {
			dlg.clear();
			result = true;
		}
	}

	if (!have8) {
		_sceneDialogFlags = static_cast<DialogFlags>(_sceneDialogFlags & ~kDlgFlagLo8);
	}
	return result;
}

bool SDSScene::drawAndUpdateDialogs(Graphics::ManagedSurface *dst) {
	bool retval = false;
	const DgdsEngine *engine = DgdsEngine::getInstance();
	for (auto &dlg : _dialogs) {
		if (dlg.hasFlag(kDlgFlagVisible) && !dlg.hasFlag(kDlgFlagLo4) &&
				!dlg.hasFlag(kDlgFlagHi20) && !dlg.hasFlag(kDlgFlagHi40)) {
			// TODO: do something with "transfer"s?
			dlg.setFlag(kDlgFlagHi4);
		}
		if (!dlg.hasFlag(kDlgFlagVisible) || (!dlg.hasFlag(kDlgFlagLo4) && !dlg.hasFlag(kDlgFlagHi4) && !dlg.hasFlag(kDlgFlagHi20) && !dlg.hasFlag(kDlgFlagHi40))) {
			if (dlg.hasFlag(kDlgFlagRedrawSelectedActionChanged) || dlg.hasFlag(kDlgFlagHi10)) {
				dlg.draw(dst, kDlgDrawStageForeground);
				if (!dlg.hasFlag(kDlgFlagRedrawSelectedActionChanged)) {
					dlg.clearFlag(kDlgFlagHi10);
				} else {
					dlg.flipFlag(kDlgFlagRedrawSelectedActionChanged);
					dlg.flipFlag(kDlgFlagHi10);
				}
			}
		} else if (!dlg.hasFlag(kDlgFlagOpening)) {
			dlg.draw(dst, kDlgDrawStageBackground);
			// HACK: always draw foreground here too..??? The original doesn't but we never
			// seem to end up calling the foreground draw function..
			dlg.draw(dst, kDlgDrawFindSelectionPointXY);
			dlg.draw(dst, kDlgDrawFindSelectionTxtOffset);
			dlg.draw(dst, kDlgDrawStageForeground);
			if (dlg.hasFlag(kDlgFlagHi20)) {
				// Reset the dialog time and selected action
				int delay = 0xffff;
				if (dlg._time)
					delay = dlg._time;

				int time = delay * 2 * (9 - engine->getTextSpeed());
				assert(dlg._state);

				dlg._state->_hideTime = DgdsEngine::getInstance()->getThisFrameMs() + time;
				dlg._state->_selectedAction = nullptr;
				dlg.updateSelectedAction(0);
				if (dlg._action.size() > 1 && !dlg._state->_selectedAction) {
					dlg._state->_selectedAction = dlg.pickAction(false, false);
					if (dlg._state->_selectedAction)
						dlg.draw(dst, kDlgDrawStageForeground);
				}
			}

			if (!dlg.hasFlag(kDlgFlagHi20)) {
				dlg.clearFlag(kDlgFlagHi40);
			} else {
				dlg.flipFlag(kDlgFlagHi20);
				dlg.flipFlag(kDlgFlagHi40);
			}
			dlg.clearFlag(kDlgFlagHi4);
			retval = true;
		} else if (!engine->justChangedScene1()) {
			dlg.clearFlag(kDlgFlagOpening);
		}

		if (dlg.hasFlag(kDlgFlagVisible) && !dlg.hasFlag(kDlgFlagLo4) &&
				!dlg.hasFlag(kDlgFlagHi20) && !dlg.hasFlag(kDlgFlagHi40)) {
			// TODO: do something with "transfer"s?
			// warning("SDSScene::drawActiveDrawAndUpdateDialogs: Do something with transfers?");
			dlg.setFlag(kDlgFlagHi4);
		}
		if (dlg.hasFlag(kDlgFlagVisible) && !dlg.hasFlag(kDlgFlagOpening)) {
			_sceneDialogFlags = static_cast<DialogFlags>(_sceneDialogFlags | kDlgFlagLo8 | kDlgFlagVisible);
		}
	}
	return retval;
}

void SDSScene::mouseUpdate(const Common::Point &pt) {
	Dialog *dlg = getVisibleDialog();
	const HotArea *area = findAreaUnderMouse(pt);
	DgdsEngine *engine = DgdsEngine::getInstance();

	int16 cursorNum = kDgdsMouseGameDefault;
	if (!dlg) {
		// Update mouse cursor if no dialog visible.
		// If lookMode is target (2) then activeItem will change it below.
		if (_lookMode)
			cursorNum = kDgdsMouseLook;
		if (area)
			cursorNum = _lookMode ? area->_cursorNum2 : area->_cursorNum;
	}

	GameItem *activeItem = engine->getGDSScene()->getActiveItem();

	if (_lbuttonDown || _rbuttonDown) {
		_mouseDownCounter++;
		if (_mouseDownCounter > MOUSE_DOWN_TIMEOUT) {
			if (_lbuttonDown && !_rbuttonDown)
				doPickUp(_mouseDownArea);
			if (activeItem && _rbuttonDown) {
				// Start target mode
				cursorNum = activeItem->_altCursor;
			}
		}
	} else {
		_mouseDownArea = nullptr;
		_mouseDownCounter = 0;
	}

	if (_dragItem) {
		if (area && area->_objInteractionRectNum == 1 && !(_dragItem->_flags & kItemStateWasInInv)) {
			// drag over Willy Beamish
			engine->getInventory()->open();
			return;
		}

		cursorNum = _dragItem->_iconNum;
	} else if (activeItem && _lookMode == 2) {
		// For Willy Beamish, target mode is sticky (look mode 2), in
		// other games it's only while mouse is down
		cursorNum = activeItem->_altCursor;
	}

	engine->setMouseCursor(cursorNum);
}

void SDSScene::mouseLDown(const Common::Point &pt) {
	_lbuttonDown = true;
	if (hasVisibleDialog()) {
		debug(9, "Mouse LDown on at %d,%d clearing visible dialog", pt.x, pt.y);
		_shouldClearDlg = true;
		_ignoreMouseUp = true;
		return;
	} else if (_dragItem) {
		// Nothing to do if we have a drag item, will be handled on mouseup.
		_lbuttonDownWithDrag = true;
		return;
	}

	_lbuttonDownWithDrag = false;
	_ignoreMouseUp = false;

	// Don't start drag in look/target mode.
	if (_lookMode)
		return;

	_mouseDownArea = findAreaUnderMouse(pt);
}

void SDSScene::doPickUp(HotArea *area) {
	if (!area)
		return;
	debug(9, "doPickUp on area %d (%d,%d,%d,%d) cursor %d cursor2 %d. Run %d ops", area->_num,
			area->_rect.x, area->_rect.y, area->_rect.width, area->_rect.height,
			area->_cursorNum, area->_cursorNum2, area->onPickUpOps.size());

	DgdsEngine *engine = DgdsEngine::getInstance();
	int16 addmins = engine->getGameGlobals()->getGameMinsToAddOnPickUp();
	runOps(area->onPickUpOps, addmins);
	GameItem *item = dynamic_cast<GameItem *>(area);
	if (item) {
		_dragItem = item;
		if (item->_inSceneNum == 2)
			item->_flags |= kItemStateWasInInv;
		if (item->_iconNum)
			engine->setMouseCursor(item->_iconNum);
	}

	_mouseDownArea = nullptr;
	_mouseDownCounter = 0;
}

static bool _isInRect(const Common::Point &pt, const DgdsRect rect) {
	return rect.x <= pt.x && (rect.x + rect.width) > pt.x
			&& rect.y <= pt.y && (rect.y + rect.height) > pt.y;
}

static const ObjectInteraction *_findInteraction(const Common::Array<ObjectInteraction> &interList, int16 droppedNum, uint16 targetNum) {
	for (const auto &i : interList) {
		if (i.matches(droppedNum, targetNum)) {
			return &i;
		}
	}
	return nullptr;
}

void SDSScene::mouseLUp(const Common::Point &pt) {
	_lbuttonDown = false;
	DgdsEngine *engine = DgdsEngine::getInstance();

	if (_ignoreMouseUp) {
		debug(9, "Ignoring mouseup at %d,%d as it was used to clear a dialog", pt.x, pt.y);
		_ignoreMouseUp = false;
		return;
	}

	//
	// HoC and Dragon drop as soon as the mouse is released.
	// Willy keeps dragging the item until another click.
	//
	if (_dragItem) {
		if (engine->getGameId() != GID_WILLY || _lbuttonDownWithDrag) {
			_dragItem->_flags &= ~kItemStateWasInInv;
			onDragFinish(pt);
			_lbuttonDownWithDrag = false;
		}
		return;
	}

	if (_lookMode == 1) {
		// Sticky look mode is on
		rightButtonAction(pt);
		return;
	}

	const HotArea *area = _mouseDownArea;
	_mouseDownArea = nullptr;
	_mouseDownCounter = 0;

	if (area) {
		debug(9, "Mouse LUp on area %d (%d,%d,%d,%d) cursor %d cursor2 %d", area->_num, area->_rect.x, area->_rect.y,
			  area->_rect.width, area->_rect.height, area->_cursorNum, area->_cursorNum2);
	} else {
		debug(9, "Mouse LUp at %d,%d with no active area", pt.x, pt.y);
	}

	const GameItem *activeItem = engine->getGDSScene()->getActiveItem();

	if (activeItem && (_rbuttonDown || _lookMode == 2)) {
		bothButtonAction(pt);
	} else {
		leftButtonAction(area);
	}
}

void SDSScene::bothButtonAction(const Common::Point &pt) {
	DgdsEngine *engine = DgdsEngine::getInstance();
	GDSScene *gds = engine->getGDSScene();
	const GameItem *activeItem = gds->getActiveItem();

	debug(1, " --> exec both-button click ops with active item %d", activeItem->_num);
	if (!runOps(activeItem->onBothButtonsOps))
		return;

	//
	// Both-button click interactions are run on *all* things the mouse
	// ignoring their "active" status.
	//

	for (const auto &hotArea : _hotAreaList) {
		if (!hotArea._rect.contains(pt))
			continue;

		const ObjectInteraction *i = _findInteraction(_objInteractions2, activeItem->_num, hotArea._num);
		if (!i)
			continue;

		debug(1, " --> exec %d both-click ops for item-area combo %d", i->opList.size(), activeItem->_num);
		if (!runOps(i->opList, engine->getGameGlobals()->getGameMinsToAddOnObjInteraction()))
			return;
	}

	for (const auto &item : gds->getGameItems()) {
		if (item._inSceneNum != _num || !item._rect.contains(pt))
			continue;

		const ObjectInteraction *i = _findInteraction(gds->getObjInteractions2(), activeItem->_num, item._num);
		if (!i)
			continue;

		debug(1, " --> exec %d both-click ops for item-item combo %d", i->opList.size(), activeItem->_num);
		if (!runOps(i->opList, engine->getGameGlobals()->getGameMinsToAddOnObjInteraction()))
			return;
	}
}

void SDSScene::leftButtonAction(const HotArea *area) {
	if (!area)
		return;

	DgdsEngine *engine = DgdsEngine::getInstance();
	GDSScene *gds = engine->getGDSScene();

	if (area->_num == 0 || area->_objInteractionRectNum == 1) {
		debug(1, "Mouse LUp on inventory.");
		engine->getInventory()->open();
	} else if (area && area->_num == 0xffff) {
		debug(1, "Mouse LUp on swap characters.");
		bool haveInvBtn = _hotAreaList.size() && _hotAreaList.front()._num == 0;
		if (haveInvBtn)
			removeInvButtonFromHotAreaList();
		int16 prevChar = gds->getGlobal(0x33);
		gds->setGlobal(0x33, gds->getGlobal(0x34));
		gds->setGlobal(0x34, prevChar);
		if (haveInvBtn)
			addInvButtonToHotAreaList();
	} else {
		debug(1, " --> exec %d use ops for area %d", area->onUseOps.size(), area->_num);
		int16 addmins = engine->getGameGlobals()->getGameMinsToAddOnUse();
		runOps(area->onUseOps, addmins);
	}
}

void SDSScene::onDragFinish(const Common::Point &pt) {
	assert(_dragItem);

	debug(9, "Drag finished at %d, %d", pt.x , pt.y);
	// Unlike a click operation, this runs the drop event for *all* areas
	// and items, ignoring enable condition.

	GameItem *dragItem = _dragItem;

	DgdsEngine *engine = DgdsEngine::getInstance();
	Globals *globals = engine->getGameGlobals();
	GDSScene *gdsScene = engine->getGDSScene();
	int16 dropSceneNum = _num;

	if (engine->getGameId() == GID_WILLY) {
		static_cast<WillyGlobals *>(globals)->setDroppedItemNum(dragItem->_num);
		if (engine->getInventory()->isOpen())
			dropSceneNum = 2;
	}

	runOps(dragItem->onDragFinishedOps, globals->getGameMinsToAddOnDrop());

	// Check for dropping on an object
	for (const auto &item : gdsScene->getGameItems()) {
		if (item._inSceneNum == dropSceneNum && _isInRect(pt, item._rect)) {
			debug(1, "Dragged item %d onto item %d @ (%d, %d)", dragItem->_num, item._num, pt.x, pt.y);
			const ObjectInteraction *i = _findInteraction(gdsScene->getObjInteractions1(), dragItem->_num, item._num);
			if (i) {
				debug(1, " --> exec %d drag ops for item %d", i->opList.size(), item._num);
				if (!runOps(i->opList, globals->getGameMinsToAddOnObjInteraction()))
					return;
			}
		}
	}

	// Check for dropping on an area
	const SDSScene *scene = engine->getScene();
	for (const auto &area : _hotAreaList) {
		if (!_isInRect(pt, area._rect))
			continue;

		if (area._num == 0) {
			debug(1, "Item %d dropped on inventory.", dragItem->_num);
			dragItem->_inSceneNum = 2;
			if (engine->getGameId() == GID_HOC)
				dragItem->_quality = Inventory::HOC_CHARACTER_QUALS[gdsScene->getGlobal(0x33)];

			const ObjectInteraction *i = _findInteraction(gdsScene->getObjInteractions1(), dragItem->_num, 0xffff);
			if (i) {
				debug(1, " --> exec %d drag ops for area %d", i->opList.size(), 0xffff);
				if (!runOps(i->opList, globals->getGameMinsToAddOnObjInteraction()))
					return;
			}
		} else if (area._num == 0xffff) {
			debug(1, "Item %d dropped on other character button.", dragItem->_num);
			dragItem->_inSceneNum = 2;
			if (engine->getGameId() == GID_HOC)
				dragItem->_quality = Inventory::HOC_CHARACTER_QUALS[gdsScene->getGlobal(0x34)];

			const ObjectInteraction *i = _findInteraction(gdsScene->getObjInteractions1(), dragItem->_num, 0xffff);
			if (i) {
				debug(1, " --> exec %d drag ops for area %d", i->opList.size(), 0xffff);
				if (!runOps(i->opList, globals->getGameMinsToAddOnObjInteraction()))
					return;
			}
		} else {
			debug(1, "Dragged item %d onto area %d @ (%d, %d)", dragItem->_num, area._num, pt.x, pt.y);
			const ObjectInteraction *i = _findInteraction(scene->getObjInteractions1(), dragItem->_num, area._num);
			if (i) {
				debug(1, " --> exec %d drag ops for area %d", i->opList.size(), area._num);
				if (!runOps(i->opList, globals->getGameMinsToAddOnObjInteraction()))
					return;
			}
		}
	}

	engine->setMouseCursor(kDgdsMouseGameDefault);
	_dragItem = nullptr;
}

void SDSScene::mouseRDown(const Common::Point &pt) {
	Dialog *dlg = getVisibleDialog();
	if (dlg) {
		// also allow right-click to clear dialogs
		_shouldClearDlg = true;
		return;
	}
	_rbuttonDown = true;
	_mouseDownArea = findAreaUnderMouse(pt);
	mouseUpdate(pt);
}

void SDSScene::mouseRUp(const Common::Point &pt) {
	if (!_rbuttonDown)
		return;

	_rbuttonDown = false;

	DgdsEngine *engine = DgdsEngine::getInstance();
	if (engine->getGameId() == GID_WILLY) {
		// Willy toggles between look/act/target mode on right click
		if (engine->getGDSScene()->getActiveItem()) {
			_lookMode++;
			if (_lookMode > 2)
				_lookMode = 0;
		} else {
			_lookMode = !_lookMode;
		}
		mouseUpdate(pt);
	} else {
		// Other games do right-button action straight away.
		bool doAction = _mouseDownCounter <= MOUSE_DOWN_TIMEOUT;
		mouseUpdate(pt);
		if (doAction)
			rightButtonAction(pt);
	}
}

void SDSScene::rightButtonAction(const Common::Point &pt) {
	const HotArea *area = findAreaUnderMouse(pt);
	if (!area)
		return;

	DgdsEngine *engine = DgdsEngine::getInstance();

	if (area->_num == 0) {
		debug(1, "Mouse RUp on inventory.");
		engine->getInventory()->setShowZoomBox(true);
		engine->getInventory()->open();
	} else if (area->_num == 0xffff) {
		debug(1, "Mouse RUp on character swap.");
		int16 swapDlgFile = engine->getGDSScene()->getGlobal(0x36);
		int16 swapDlgNum = engine->getGDSScene()->getGlobal(0x35);
		if (swapDlgFile && swapDlgNum)
			showDialog(swapDlgFile, swapDlgNum);
	} else {
		doLook(area);
	}
}

void SDSScene::doLook(const HotArea *area) {
	if (!area)
		return;
	DgdsEngine *engine = DgdsEngine::getInstance();
	int16 addmins = engine->getGameGlobals()->getGameMinsToAddOnUse();
	debug(1, "doLook on area %d, run %d ops (+%d mins)", area->_num, area->onLookOps.size(), addmins);
	runOps(area->onLookOps, addmins);
}

Dialog *SDSScene::getVisibleDialog() {
	for (auto &dlg : _dialogs) {
		if (dlg.hasFlag(kDlgFlagVisible) && !dlg.hasFlag(kDlgFlagOpening)) {
			return &dlg;
		}
	}
	return nullptr;
}

bool SDSScene::hasVisibleDialog() {
	return getVisibleDialog() != nullptr;
}

bool SDSScene::hasVisibleOrOpeningDialog() const {
	for (const auto &dlg : _dialogs) {
		if (dlg.hasFlag(kDlgFlagVisible) || dlg.hasFlag(kDlgFlagOpening)) {
			return true;
		}
	}
	return false;
}

void SDSScene::setDynamicSceneRect(int16 num, int16 x, int16 y, int16 width, int16 height) {
	for (auto &dynamicRect : _dynamicRects) {
		if (dynamicRect._num == num) {
			dynamicRect._rect = DgdsRect(x, y, width, height);
			return;
		}
	}

	_dynamicRects.push_back(DynamicRect());
	_dynamicRects.back()._num = num;
	_dynamicRects.back()._rect = DgdsRect(x, y, width, height);
}


void SDSScene::updateHotAreasFromDynamicRects() {
	if (_dynamicRects.empty())
		return;
	for (auto &hotArea : _hotAreaList) {
		if (!hotArea._objInteractionRectNum)
			continue;
		for (const auto &dynamicRect : _dynamicRects) {
			if (hotArea._objInteractionRectNum == dynamicRect._num) {
				hotArea._rect = dynamicRect._rect;
				break;
			}
		}
	}
}

HotArea *SDSScene::findAreaUnderMouse(const Common::Point &pt) {
	for (auto &item : DgdsEngine::getInstance()->getGDSScene()->getGameItems()) {
		if (item._inSceneNum == _num && _isInRect(pt, item._rect)
			&& SceneConditions::check(item.enableConditions)) {
			return &item;
		}
	}

	for (auto &area : _hotAreaList) {
		if (_isInRect(pt, area._rect) && SceneConditions::check(area.enableConditions)) {
			return &area;
		}
	}
	return nullptr;
}

void SDSScene::addInvButtonToHotAreaList() {
	DgdsEngine *engine = DgdsEngine::getInstance();
	const Common::Array<MouseCursor> &cursors = engine->getGDSScene()->getCursorList();
	const Common::SharedPtr<Image> &icons = engine->getIcons();

	if (cursors.empty() || !icons || icons->loadedFrameCount() <= 2 || _num == 2)
		return;

	if (_hotAreaList.size() && _hotAreaList.front()._num == 0)
		return;

	int16 invButtonIcon = engine->getGDSScene()->getInvIconNum();
	if (engine->getGameId() == GID_HOC) {
		static const byte HOC_INV_ICONS[] = { 0, 2, 18, 19 };
		invButtonIcon = HOC_INV_ICONS[engine->getGDSScene()->getGlobal(0x33)];
	}

	HotArea area;
	area._num = 0;
	area._cursorNum = engine->getGDSScene()->getInvIconMouseCursor();
	area._rect.width = icons->width(invButtonIcon);
	area._rect.height = icons->height(invButtonIcon);
	area._rect.x = SCREEN_WIDTH - area._rect.width;
	area._rect.y = SCREEN_HEIGHT - area._rect.height;
	area._cursorNum2 = engine->getGDSScene()->getInvIconMouseCursor();
	area._objInteractionRectNum = 0;

	// Add swap character button for HoC
	if (engine->getGameId() == GID_HOC && engine->getGDSScene()->getGlobal(0x34) != 0) {
		int16 charNum = engine->getGDSScene()->getGlobal(0x34);
		int16 iconNum = DgdsEngine::HOC_CHAR_SWAP_ICONS[charNum];
		HotArea area2;
		area2._num = 0xffff;
		area2._cursorNum = 0;
		area2._rect.width = icons->width(iconNum);
		area2._rect.height = icons->height(iconNum);
		area2._rect.x = 5;
		area2._rect.y = SCREEN_HEIGHT - area2._rect.height - 5;
		area2._cursorNum2 = 0;
		area2._objInteractionRectNum = 0;

		_hotAreaList.push_front(area2);
	}

	_hotAreaList.push_front(area);
}

void SDSScene::removeInvButtonFromHotAreaList() {
	if (_hotAreaList.size() && _hotAreaList.front()._num == 0)
		_hotAreaList.pop_front();
	// Also remove character swap button in HoC
	if (_hotAreaList.size() && _hotAreaList.front()._num == 0xffff)
		_hotAreaList.pop_front();
}

Common::Error SDSScene::syncState(Common::Serializer &s) {
	// num should be synced as part of the engine -
	// at this point we are already loaded.
	assert(_num);

	// The dialogs and triggers are stateful, everything else is stateless.
	uint16 ndlgs = _dialogs.size();
	s.syncAsUint16LE(ndlgs);
	if (_dialogs.size() && ndlgs != _dialogs.size()) {
		error("Dialog count in save doesn't match count in game (%d vs %d)",
				ndlgs, _dialogs.size());
	} else if (_dialogs.size()) {
		for (auto &dlg : _dialogs) {
			dlg.syncState(s);
		}
	} else if (ndlgs && s.isLoading()) {
		warning("Skipping dialog data in save");
		Dialog dlg;
		for (uint i = 0; i < ndlgs; i++)
			dlg.syncState(s);
	}

	uint16 ntrig = _triggers.size();
	s.syncAsUint16LE(ntrig);
	if (ntrig != _triggers.size()) {
		error("Trigger count in save doesn't match count in game (%d vs %d)",
				ntrig, _triggers.size());
	}
	for (auto &trg : _triggers)
		s.syncAsByte(trg._enabled);

	return Common::kNoError;
}

void SDSScene::prevChoice() {
	Dialog *dlg = getVisibleDialog();
	if (!dlg)
		return;
	dlg->updateSelectedAction(-1);
}

void SDSScene::nextChoice() {
	Dialog *dlg = getVisibleDialog();
	if (!dlg)
		return;
	dlg->updateSelectedAction(1);
}

void SDSScene::activateChoice() {
	Dialog *dlg = getVisibleDialog();
	if (!dlg)
		return;
	_shouldClearDlg = true;
}

void SDSScene::drawDebugHotAreas(Graphics::ManagedSurface &dst) const {
	const DgdsPal &pal = DgdsEngine::getInstance()->getGamePals()->getCurPal();
	byte redish = pal.findBestColor(0xff, 0, 0);
	byte greenish = pal.findBestColor(0, 0xff, 0);

	for (const auto &area : _hotAreaList) {
		bool enabled = SceneConditions::check(area.enableConditions);
		uint32 color = enabled ? greenish : redish;
		g_system->getPaletteManager();
		const Common::Rect &r = area._rect.toCommonRect();
		dst.drawLine(r.left, r.top, r.right, r.top, color);
		dst.drawLine(r.left, r.top, r.left, r.bottom, color);
		dst.drawLine(r.left, r.bottom, r.right, r.bottom, color);
		dst.drawLine(r.right, r.top, r.right, r.bottom, color);
	}
}

GDSScene::GDSScene() : _defaultMouseCursor(0), _defaultMouseCursor2(0), _invIconNum(0), _invIconMouseCursor(0), _defaultOtherMouseCursor(0) {
}

bool GDSScene::load(const Common::String &filename, ResourceManager *resourceManager, Decompressor *decompressor) {
	Common::SeekableReadStream *sceneFile = resourceManager->getResource(filename);
	if (!sceneFile)
		error("Scene file %s not found", filename.c_str());

	DgdsChunkReader chunk(sceneFile);

	bool result = false;

	while (chunk.readNextHeader(EX_GDS, filename)) {
		if (chunk.isContainer()) {
			continue;
		}

		chunk.readContent(decompressor);
		Common::SeekableReadStream *stream = chunk.getContent();

		if (chunk.isSection(ID_GDS)) {
			// do nothing, this is the container.
			assert(chunk.isContainer());
		} else if (chunk.isSection(ID_INF)) {
			result = parseInf(stream);
		} else if (chunk.isSection(ID_SDS)) {
			result = parse(stream);
		}
	}

	initIconSizes();

	delete sceneFile;

	return result;
}

bool GDSScene::loadRestart(const Common::String &filename, ResourceManager *resourceManager, Decompressor *decompressor) {
	Common::SeekableReadStream *file = resourceManager->getResource(filename);
	if (!file)
		error("Restart data %s not found", filename.c_str());

	uint32 magic = file->readUint32LE();
	if (magic != _magic)
		error("Restart file magic doesn't match (%04X vs %04X)", magic, _magic);

	uint16 num = file->readUint16LE();
	// Find matching game item and load its values
	while (num && !file->eos()) {
		bool found = false;
		for (GameItem &item : _gameItems) {
			if (item._num == num) {
				item._rect.x = file->readUint16LE();
				item._rect.y = file->readUint16LE();
				item._rect.width = file->readUint16LE();
				item._rect.height = file->readUint16LE();
				item._inSceneNum = file->readUint16LE();
				item._flags = file->readUint16LE();
				item._quality = file->readUint16LE();
				found = true;
				break;
			}
		}
		if (!found)
			error("Reset file references unknown item %d", num);
		num = file->readUint16LE();
	}
	initIconSizes();

	DgdsEngine *engine = DgdsEngine::getInstance();
	Common::Array<Global *> &globs = engine->getGameGlobals()->getAllGlobals();

	if (engine->getGameId() == GID_DRAGON || engine->getGameId() == GID_HOC) {
		num = file->readUint16LE();
		while (num && !file->eos()) {
			uint16 scene = file->readUint16LE();
			int16 val = file->readSint16LE();
			bool found = false;
			for (PerSceneGlobal &glob : _perSceneGlobals) {
				if (glob.matches(num, scene)) {
					glob._val = val;
					found = true;
					break;
				}
			}
			if (!found)
				error("Reset file references unknown scene global %d", num);
			num = file->readUint16LE();
		}

		/*uint32 unk = */ file->readUint32LE();

		if (globs.size() > 50)
			error("Too many globals to load from RST file");

		int g = 0;
		for (Global *glob : globs) {
			int16 val = file->readUint16LE();
			glob->setRaw(val);
			g++;
		}
		// Always 50 int16s worth of globals in the file, skip any unused.
		if (g < 50)
			file->skip(2 * (50 - g));

		uint16 triggers[100];
		for (int i = 0; i < ARRAYSIZE(triggers); i++) {
			triggers[i] = file->readUint16LE();
		}

		engine->_compositionBuffer.fillRect(Common::Rect(SCREEN_WIDTH, SCREEN_HEIGHT), 0);
		// TODO: FIXME: What should this scene num be? For now hacked to work with Dragon.
		engine->changeScene(3);
		SDSScene *scene = engine->getScene();
		int t = 0;
		num = triggers[t++];
		while (num) {
			uint16 val = triggers[t++];
			scene->enableTrigger(0, num, (bool)val);
			num = triggers[t++];
		}
	} else {
		// Willy Beamish stores the globals differently
		num = file->readUint16LE();
		while (num && !file->eos()) {
			int16 val = file->readSint16LE();
			bool found = false;
			for (PerSceneGlobal &glob : _perSceneGlobals) {
				if (glob.numMatches(num)) {
					glob._val = val;
					found = true;
					break;
				}
			}
			if (!found)
				error("Reset file references unknown scene global %d", num);
			num = file->readUint16LE();
		}

		/*uint32 unk = */ file->readUint32LE();

		num = file->readUint16LE();
		while (num && !file->eos()) {
			bool found = false;
			int16 val = file->readUint16LE();
			for (Global *glob : globs) {
				if (glob->getNum() == num) {
					glob->setRaw(val);
					found = true;
					break;
				}
			}
			if (!found)
				error("Reset file references unknown game global %d", num);
			num = file->readUint16LE();
		}

		//
		// TODO: What is this block of data?  In practice there is only one of them
		//
		while (!file->eos()) {
			num = file->readUint16LE();
			if (!num)
				break;
			/*int16 val1 = */ file->readUint16LE();
			/*int16 val2 = */ file->readUint16LE();
			/*int16 val3 = */ file->readUint16LE();
		}

		/*uint16 soundBankNum = */ file->readUint16LE();

		engine->_compositionBuffer.fillRect(Common::Rect(SCREEN_WIDTH, SCREEN_HEIGHT), 0);
		// TODO: FIXME: What should this scene num be?
		engine->changeScene(3);
	}

	return true;
}

void GDSScene::initIconSizes() {
	const Common::SharedPtr<Image> icons = DgdsEngine::getInstance()->getIcons();
	uint16 nicons = icons ? icons->getFrames().size() : 0;
	for (GameItem &item : _gameItems) {
		if (item._iconNum < nicons) {
			item._rect.width = icons->getFrames()[item._iconNum]->w;
			item._rect.height = icons->getFrames()[item._iconNum]->h;
		} else {
			item._rect.width = 32;
			item._rect.height = 32;
		}
	}
}

bool GDSScene::readPerSceneGlobals(Common::SeekableReadStream *s) {
	uint16 numGlobals = s->readUint16LE();
	uint16 num, scene;
	for (uint16 i = 0; i < numGlobals; i++) {
		num = s->readUint16LE();
		scene = s->readUint16LE();
		_perSceneGlobals.push_back(PerSceneGlobal(num, scene));
		_perSceneGlobals.back()._val = s->readSint16LE();
	}
	return !s->err();
}

bool GDSScene::parseInf(Common::SeekableReadStream *s) {
	_magic = s->readUint32LE();
	_version = s->readString();
	return !s->err();
}

bool GDSScene::parse(Common::SeekableReadStream *stream) {
	readOpList(stream, _startGameOps);
	readOpList(stream, _quitGameOps);
	if (isVersionOver(" 1.206"))
		readOpList(stream, _preTickOps);
	readOpList(stream, _postTickOps);
	if (isVersionOver(" 1.208"))
		readOpList(stream, _onChangeSceneOps);
	readPerSceneGlobals(stream);
	_iconFile = stream->readString();
	readMouseHotspotList(stream, _cursorList);
	readGameItemList(stream, _gameItems);
	readObjInteractionList(stream, _objInteractions1);
	if (isVersionOver(" 1.205"))
		readObjInteractionList(stream, _objInteractions2);

	if (isVersionOver(" 1.218")) {
		_defaultMouseCursor = stream->readSint16LE();
		_defaultMouseCursor2 = stream->readUint16LE();
		_invIconNum = stream->readUint16LE();
		_invIconMouseCursor = stream->readSint16LE();
		_defaultOtherMouseCursor = stream->readSint16LE();
	} else {
		_defaultMouseCursor = 0;
		_defaultMouseCursor2 = 1;
		_invIconNum = 2;
		_invIconMouseCursor = 0;
		_defaultOtherMouseCursor = 6;
	}

	return !stream->err();
}

Common::String GDSScene::dump(const Common::String &indent) const {
	Common::String str = Common::String::format("%sGDSScene<ver %s icons %s", indent.c_str(), _version.c_str(), _iconFile.c_str());
	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 + ">";
	return str;
}

void GDSScene::globalOps(const Common::Array<uint16> &args) {
	if (!args.size()) {
		// This happens in Willy Beamish CD version when vacuuming
		// up the babysitter (D50.DDS, dialog num 54)
		warning("GDSScene::globalOps: Empty arg list");
		return;
	}

	// The arg list should be a first value giving the count of operations,
	// then 3 values for each op (num, opcode, val).
	uint nops = args.size() / 3;
	uint nops_in_args = args[0];
	if (args.size() != nops * 3 + 1 || nops != nops_in_args)
		error("GDSScene::globalOps: Op list should be length 3*n+1");

	for (uint i = 0; i < nops; i++) {
		uint16 num = args[i * 3 + 1];
		uint16 op  = args[i * 3 + 2];
		int16 val  = args[i * 3 + 3];

		// CHECK ME: The original uses a different function here, but the
		// result appears to be the same as just calling getGlobal?
		int16 num2 = getGlobal(num);

		// Op bit 3 on means use absolute val of val.
		// Off means val is another global to lookup
		if (op & 8)
			op = op & 0xfff7;
		else
			val = getGlobal((uint16)val);

		if (op == 1)
			val = num2 + val;
		else if (op == 6)
			val = (val == 0);
		else if (op == 5)
			val = num2 - val;

		setGlobal(num, val);
	}
}

int16 GDSScene::getGlobal(uint16 num) const {
	DgdsEngine *engine = DgdsEngine::getInstance();
	int curSceneNum = engine->getScene()->getNum();
	DgdsGameId gameId = engine->getGameId();

	for (const auto &global : _perSceneGlobals) {
		if (global.matches(num, curSceneNum))
			return global._val;
		else if (!global.matches(num, curSceneNum) && global.numMatches(num)) {
			// Don't warn on known reusable scene globals
			if (gameId == GID_WILLY && num == 185)
				return global._val;

			// This looks like a script bug, get it anyway
			warning("getGlobal: scene global %d is not in scene %d", num, curSceneNum);
			return global._val;
		}
	}
	Globals *gameGlobals = engine->getGameGlobals();
	return gameGlobals->getGlobal(num);
}

int16 GDSScene::setGlobal(uint16 num, int16 val) {
	DgdsEngine *engine = DgdsEngine::getInstance();
	int curSceneNum = engine->getScene()->getNum();
	for (auto &global : _perSceneGlobals) {
		if (global.matches(num, curSceneNum)) {
			global._val = val;
			return val;
		} else if (!global.matches(num, curSceneNum) && global.numMatches(num)) {
			// This looks like a script bug, set it anyway
			warning("setGlobal: scene global %d is not in scene %d", num, curSceneNum);
			global._val = val;
			return val;
		}
	}
	Globals *gameGlobals = engine->getGameGlobals();
	return gameGlobals->setGlobal(num, val);
}

void GDSScene::drawItems(Graphics::ManagedSurface &surf) {
	DgdsEngine *engine = DgdsEngine::getInstance();
	const Common::SharedPtr<Image> &icons = engine->getIcons();
	int currentScene = engine->getScene()->getNum();
	if (!icons || icons->loadedFrameCount() < 3)
		return;

	int xoff = 20;
	const Common::Rect screenWin(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
	// Don't overlap the inventory icon.
	const int maxx = SCREEN_WIDTH - (icons->width(2) + 10);
	for (auto &item : _gameItems) {
		if (item._inSceneNum == currentScene && &item != engine->getScene()->getDragItem()) {
			if (!(item._flags & kItemStateDragging)) {
				// Dropped item.
				if (xoff + item._rect.width > maxx)
					xoff = 20;
				int yoff = SCREEN_HEIGHT - (item._rect.height + 2);
				item._rect.x = xoff;
				item._rect.y = yoff;
				icons->drawBitmap(item._iconNum, xoff, yoff, screenWin, surf);
				xoff += (item._rect.width + 6);
			} else {
				icons->drawBitmap(item._iconNum, item._rect.x, item._rect.y, screenWin, surf);
			}
		}
	}
}

int GDSScene::countItemsInInventory() const {
	int result = 0;
	bool isHoc = DgdsEngine::getInstance()->getGameId() == GID_HOC;
	for (const auto &item : _gameItems) {
		if (item._inSceneNum == 2) {
			if (isHoc) {
				int16 currentCharacter = getGlobal(0x33);
				if (item._quality == Inventory::HOC_CHARACTER_QUALS[currentCharacter])
					result++;
			} else {
				result++;
			}
		}
	}
	return result;
}

GameItem *GDSScene::getActiveItem() {
	int16 itemNum = getGlobal(0x60);
	if (itemNum <= 0)
		return nullptr;
	for (auto &item : _gameItems) {
		if (item._num == (uint16)itemNum)
			return &item;
	}
	return nullptr;
}

Common::Error GDSScene::syncState(Common::Serializer &s) {
	// Only items and globals are stateful - everything else is stateless.
	// Game should already be loaded at this point so the lsits are already
	// filled out.

	assert(!_gameItems.empty());
	assert(!_perSceneGlobals.empty());

	// TODO: Maybe it would be nicer to save the item/global numbers
	// with the values in case the order changed in some other version of
	// the game data?  This assumes they will be the same order.

	uint16 nitems = _gameItems.size();
	s.syncAsUint16LE(nitems);
	if (nitems != _gameItems.size()) {
		error("Item count in save doesn't match count in game (%d vs %d)",
				nitems, _gameItems.size());
	}
	for (GameItem &item : _gameItems) {
		s.syncAsUint16LE(item._inSceneNum);
		if (s.getVersion() > 1)
			s.syncAsUint16LE(item._flags);
		s.syncAsUint16LE(item._quality);
		//debug(1, "loaded item: %d %d %d %d", item._num, item._inSceneNum, item._flags, item._quality);
	}

	uint16 nglobals = _perSceneGlobals.size();
	s.syncAsUint16LE(nglobals);
	if (nglobals != _perSceneGlobals.size()) {
		error("Scene global count in save doesn't match count in game (%d vs %d)",
				nglobals, _perSceneGlobals.size());
	}
	for (PerSceneGlobal &glob : _perSceneGlobals) {
		s.syncAsUint16LE(glob._val);
	}

	return Common::kNoError;
}

} // End of namespace Dgds
