scummvm/engines/dgds/scene.cpp
2025-03-10 18:53:20 +02:00

2214 lines
65 KiB
C++

/* 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