scummvm/engines/mohawk/livingbooks.cpp
besentv 3530eae6f1 MOHAWK: Fix game "Just Grandma and Me" german and french version crash
The german and french version of "Just Grandma and Me" (Mohawk engine) have the special characters "ç" and "ö" in their config files. This causes ScummVM to crash when trying to start the game in german or french. Allowing non english chars in config files for Living Books games fixes this issue.
2020-10-31 14:06:25 +01:00

3987 lines
99 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 2
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
*/
#include "mohawk/livingbooks.h"
#include "mohawk/resource.h"
#include "mohawk/cursors.h"
#include "mohawk/video.h"
#include "common/config-manager.h"
#include "common/error.h"
#include "common/events.h"
#include "common/fs.h"
#include "common/archive.h"
#include "common/textconsole.h"
#include "common/system.h"
#include "common/memstream.h"
#include "graphics/palette.h"
#include "engines/util.h"
#include "gui/message.h"
#include "graphics/cursorman.h"
namespace Mohawk {
// read a null-terminated string from a stream
Common::String MohawkEngine_LivingBooks::readString(Common::ReadStream *stream) {
Common::String ret;
while (!stream->eos()) {
byte in = stream->readByte();
if (!in)
break;
ret += in;
}
return ret;
}
// read a rect from a stream
Common::Rect MohawkEngine_LivingBooks::readRect(Common::ReadStreamEndian *stream) {
Common::Rect rect;
// the V1 mac games have their rects in QuickDraw order
if (isPreMohawk() && getPlatform() == Common::kPlatformMacintosh) {
rect.top = stream->readSint16();
rect.left = stream->readSint16();
rect.bottom = stream->readSint16();
rect.right = stream->readSint16();
} else {
rect.left = stream->readSint16();
rect.top = stream->readSint16();
rect.right = stream->readSint16();
rect.bottom = stream->readSint16();
}
return rect;
}
LBPage::LBPage(MohawkEngine_LivingBooks *vm) : _vm(vm) {
_code = NULL;
_mhk = NULL;
_baseId = 0;
_cascade = false;
}
void LBPage::open(Archive *mhk, uint16 baseId) {
_mhk = mhk;
_baseId = baseId;
_vm->addArchive(_mhk);
if (!_vm->hasResource(ID_BCOD, baseId)) {
// assume that BCOD is mandatory for v4/v5
if (_vm->getGameType() == GType_LIVINGBOOKSV4 || _vm->getGameType() == GType_LIVINGBOOKSV5)
error("missing BCOD resource (id %d)", baseId);
_code = new LBCode(_vm, 0);
} else {
_code = new LBCode(_vm, baseId);
}
loadBITL(baseId);
for (uint i = 0; i < _items.size(); i++)
_vm->addItem(_items[i]);
for (uint32 i = 0; i < _items.size(); i++)
_items[i]->init();
for (uint32 i = 0; i < _items.size(); i++)
_items[i]->startPhase(kLBPhaseLoad);
}
void LBPage::addClonedItem(LBItem *item) {
_vm->addItem(item);
_items.push_back(item);
}
void LBPage::itemDestroyed(LBItem *item) {
for (uint i = 0; i < _items.size(); i++)
if (item == _items[i]) {
_items.remove_at(i);
return;
}
error("itemDestroyed didn't find item");
}
LBPage::~LBPage() {
delete _code;
_vm->removeItems(_items);
for (uint i = 0; i < _items.size(); i++)
delete _items[i];
_vm->removeArchive(_mhk);
delete _mhk;
}
MohawkEngine_LivingBooks::MohawkEngine_LivingBooks(OSystem *syst, const MohawkGameDescription *gamedesc) : MohawkEngine(syst, gamedesc) {
_needsUpdate = false;
_needsRedraw = false;
_screenWidth = _screenHeight = 0;
_curLanguage = 1;
_curSelectedPage = 1;
_alreadyShowedIntro = false;
_rnd = new Common::RandomSource("livingbooks");
_sound = NULL;
_video = NULL;
_page = NULL;
const Common::FSNode gameDataDir(ConfMan.get("path"));
// Rugrats
SearchMan.addSubDirectoryMatching(gameDataDir, "program", 0, 2);
SearchMan.addSubDirectoryMatching(gameDataDir, "Rugrats Adventure Game", 0, 2);
// CarmenTQ
SearchMan.addSubDirectoryMatching(gameDataDir, "95instal", 0, 4);
}
MohawkEngine_LivingBooks::~MohawkEngine_LivingBooks() {
destroyPage();
delete _sound;
delete _video;
delete _gfx;
delete _rnd;
_bookInfoFile.clear();
}
Common::Error MohawkEngine_LivingBooks::run() {
MohawkEngine::run();
if (!_mixer->isReady()) {
return Common::kAudioDeviceInitFailed;
}
setDebugger(new LivingBooksConsole(this));
// Load the book info from the detected file
loadBookInfo(getBookInfoFileName());
if (!_title.empty()) // Some games don't have the title stored
debug("Starting Living Books Title \'%s\'", _title.c_str());
if (!_copyright.empty())
debug("Copyright: %s", _copyright.c_str());
debug("This book has %d page(s) in %d language(s).", _numPages, _numLanguages);
if (_poetryMode)
debug("Running in poetry mode.");
if (!_screenWidth || !_screenHeight)
error("Could not find xRes/yRes variables");
_gfx = new LBGraphics(this, _screenWidth, _screenHeight);
_video = new VideoManager(this);
_sound = new Sound(this);
if (getGameType() != GType_LIVINGBOOKSV1)
_cursor = new LivingBooksCursorManager_v2();
else if (getPlatform() == Common::kPlatformMacintosh)
_cursor = new MacCursorManager(getAppName());
else
_cursor = new NECursorManager(getAppName());
_cursor->setDefaultCursor();
_cursor->showCursor();
if (!tryLoadPageStart(kLBIntroMode, 1))
error("Could not load intro page");
Common::Event event;
while (!shouldQuit()) {
while (_eventMan->pollEvent(event)) {
LBItem *found = NULL;
switch (event.type) {
case Common::EVENT_MOUSEMOVE:
_needsUpdate = true;
break;
case Common::EVENT_LBUTTONUP:
if (_focus)
_focus->handleMouseUp(event.mouse);
break;
case Common::EVENT_LBUTTONDOWN:
for (Common::List<LBItem *>::const_iterator i = _orderedItems.begin(); i != _orderedItems.end(); ++i) {
if ((*i)->contains(event.mouse)) {
found = *i;
break;
}
}
if (found && CursorMan.isVisible())
found->handleMouseDown(event.mouse);
break;
case Common::EVENT_KEYDOWN:
switch (event.kbd.keycode) {
case Common::KEYCODE_SPACE:
pauseGame();
break;
case Common::KEYCODE_ESCAPE:
if (_curMode == kLBIntroMode)
tryLoadPageStart(kLBControlMode, 1);
else
_video->stopVideos();
break;
case Common::KEYCODE_LEFT:
prevPage();
break;
case Common::KEYCODE_RIGHT:
nextPage();
break;
default:
break;
}
break;
default:
break;
}
}
updatePage();
if (_video->updateMovies())
_needsUpdate = true;
if (_needsUpdate) {
_system->updateScreen();
_needsUpdate = false;
}
// Cut down on CPU usage
_system->delayMillis(10);
// handle pending notifications
while (_notifyEvents.size()) {
NotifyEvent notifyEvent = _notifyEvents.pop();
handleNotify(notifyEvent);
}
}
return Common::kNoError;
}
void MohawkEngine_LivingBooks::pauseEngineIntern(bool pause) {
MohawkEngine::pauseEngineIntern(pause);
if (pause) {
_video->pauseVideos();
} else {
_video->resumeVideos();
_system->updateScreen();
}
}
void MohawkEngine_LivingBooks::loadBookInfo(const Common::String &filename) {
_bookInfoFile.allowNonEnglishCharacters();
if (!_bookInfoFile.loadFromFile(filename))
error("Could not open %s as a config file", filename.c_str());
_title = getStringFromConfig("BookInfo", "title");
_copyright = getStringFromConfig("BookInfo", "copyright");
_numPages = getIntFromConfig("BookInfo", "nPages");
_numLanguages = getIntFromConfig("BookInfo", "nLanguages");
_screenWidth = getIntFromConfig("BookInfo", "xRes");
_screenHeight = getIntFromConfig("BookInfo", "yRes");
// nColors is here too, but it's always 256 anyway...
// this is 1 in The New Kid on the Block, changes the hardcoded UI
// v2 games changed the flag name to fPoetry
if (getGameType() == GType_LIVINGBOOKSV1)
_poetryMode = (getIntFromConfig("BookInfo", "poetry") == 1);
else
_poetryMode = (getIntFromConfig("BookInfo", "fPoetry") == 1);
// The later Living Books games add some more options:
// - fNeedPalette (always true?)
// - fUse254ColorPalette (always true?)
// - nKBRequired (4096, RAM requirement?)
// - fDebugWindow (always 0?)
if (_bookInfoFile.hasSection("Globals")) {
const Common::INIFile::SectionKeyList globals = _bookInfoFile.getKeys("Globals");
for (Common::INIFile::SectionKeyList::const_iterator i = globals.begin(); i != globals.end(); i++) {
Common::String command = Common::String::format("%s = %s", i->key.c_str(), i->value.c_str());
LBCode tempCode(this, 0);
uint offset = tempCode.parseCode(command);
tempCode.runCode(NULL, offset);
}
}
}
Common::String MohawkEngine_LivingBooks::stringForMode(LBMode mode) {
Common::String language = getStringFromConfig("Languages", Common::String::format("Language%d", _curLanguage));
switch (mode) {
case kLBIntroMode:
return "Intro";
case kLBControlMode:
return "Control";
case kLBCreditsMode:
return "Credits";
case kLBPreviewMode:
return "Preview";
case kLBReadMode:
return language + ".Read";
case kLBPlayMode:
return language + ".Play";
default:
error("unknown game mode %d", (int)mode);
}
}
void MohawkEngine_LivingBooks::destroyPage() {
_sound->stopSound();
_lastSoundOwner = 0;
_lastSoundId = 0;
_soundLockOwner = 0;
_gfx->clearCache();
_video->stopVideos();
_eventQueue.clear();
delete _page;
assert(_items.empty());
assert(_orderedItems.empty());
_page = NULL;
_notifyEvents.clear();
_focus = NULL;
}
// Replace any colons (originally a slash) with another character
static Common::String replaceColons(const Common::String &in, char replace) {
Common::String out;
for (uint32 i = 0; i < in.size(); i++) {
if (in[i] == ':')
out += replace;
else
out += in[i];
}
return out;
}
// Helper function to assist in opening pages
static bool tryOpenPage(Archive *archive, const Common::String &fileName) {
// Try the plain file name first
if (archive->openFile(fileName))
return true;
// No colons, then bail out
if (!fileName.contains(':'))
return false;
// Try replacing colons with underscores (in case the original was
// a Mac version and had slashes not as a separator).
if (archive->openFile(replaceColons(fileName, '_')))
return true;
// Try replacing colons with slashes (in case the original was a Mac
// version and had slashes as a separator).
if (archive->openFile(replaceColons(fileName, '/')))
return true;
// Failed to open the archive
return false;
}
bool MohawkEngine_LivingBooks::loadPage(LBMode mode, uint page, uint subpage) {
destroyPage();
Common::String name = stringForMode(mode);
Common::String base;
if (subpage)
base = Common::String::format("Page%d.%d", page, subpage);
else
base = Common::String::format("Page%d", page);
Common::String filename, leftover;
filename = getFileNameFromConfig(name, base, leftover);
_readOnly = false;
if (filename.empty()) {
leftover.clear();
filename = getFileNameFromConfig(name, base + ".r", leftover);
_readOnly = true;
}
// TODO: fading between pages
#if 0
bool fade = false;
if (leftover.contains("fade")) {
fade = true;
}
#endif
if (leftover.contains("read")) {
_readOnly = true;
}
if (leftover.contains("load")) {
// FIXME: don't ignore this
warning("ignoring 'load' for filename '%s'", filename.c_str());
}
if (leftover.contains("cut")) {
// FIXME: don't ignore this
warning("ignoring 'cut' for filename '%s'", filename.c_str());
}
if (leftover.contains("killgag")) {
// FIXME: don't ignore this
warning("ignoring 'killgag' for filename '%s'", filename.c_str());
}
Archive *pageArchive = createArchive();
if (!filename.empty() && tryOpenPage(pageArchive, filename)) {
_page = new LBPage(this);
_page->open(pageArchive, 1000);
} else {
delete pageArchive;
debug(2, "Could not find page %d.%d for '%s'", page, subpage, name.c_str());
return false;
}
if (getFeatures() & GF_LB_10) {
if (_readOnly) {
error("found .r entry in Living Books 1.0 game");
} else {
// some very early versions of the LB engine don't have
// .r entries in their book info; instead, it is just hardcoded
// like this (which would unfortunately break later games)
_readOnly = (mode != kLBControlMode && mode != kLBPlayMode);
}
}
debug(1, "Page Version: %d", _page->getResourceVersion());
_curMode = mode;
_curPage = page;
_curSubPage = subpage;
_cursor->showCursor();
_gfx->setPalette(1000);
_phase = 0;
_introDone = false;
_needsRedraw = true;
return true;
}
void MohawkEngine_LivingBooks::updatePage() {
switch (_phase) {
case kLBPhaseInit:
for (uint32 i = 0; i < _items.size(); i++)
_items[i]->startPhase(kLBPhaseCreate);
for (uint32 i = 0; i < _items.size(); i++)
_items[i]->startPhase(_phase);
if (_curMode == kLBControlMode) {
// hard-coded control page startup
LBItem *item;
uint16 page = _curPage;
if (getFeatures() & GF_LB_10) {
// Living Books 1.0 had the meanings of these pages reversed
if (page == 2)
page = 3;
else if (page == 3)
page = 2;
}
switch (page) {
case 1:
debug(2, "updatePage() for control page 1 (menu)");
if (_poetryMode) {
for (uint16 i = 0; i < _numPages; i++) {
item = getItemById(1000 + i);
if (item)
item->setVisible(_curSelectedPage == i + 1);
item = getItemById(1100 + i);
if (item)
item->setVisible(_curSelectedPage == i + 1);
}
}
for (uint16 i = 0; i < _numLanguages; i++) {
item = getItemById(100 + i);
if (item)
item->seek((i + 1 == _curLanguage) ? 0xFFFF : 1);
item = getItemById(200 + i);
if (item)
item->setVisible(false);
}
item = getItemById(12);
if (item)
item->setVisible(false);
if (_alreadyShowedIntro) {
item = getItemById(10);
if (item) {
item->setVisible(false);
item->seek(0xFFFF);
}
} else {
_alreadyShowedIntro = true;
item = getItemById(11);
if (item)
item->setVisible(false);
}
break;
case 2:
debug(2, "updatePage() for control page 2 (quit)");
item = getItemById(12);
if (item)
item->setVisible(false);
item = getItemById(13);
if (item)
item->setVisible(false);
break;
case 3:
debug(2, "updatePage() for control page 3 (options)");
for (uint i = 0; i < _numLanguages; i++) {
item = getItemById(100 + i);
if (item)
item->setVisible(_curLanguage == i + 1);
}
for (uint i = 0; i < _numPages; i++) {
item = getItemById(1000 + i);
if (item)
item->setVisible(_curSelectedPage == i + 1);
item = getItemById(1100 + i);
if (item)
item->setVisible(_curSelectedPage == i + 1);
}
item = getItemById(202);
if (item)
item->setVisible(false);
break;
default:
break;
}
}
_phase++;
break;
case kLBPhaseIntro:
for (uint32 i = 0; i < _items.size(); i++)
_items[i]->startPhase(_phase);
if (_curMode == kLBControlMode) {
LBItem *item = getItemById(10);
if (item)
item->togglePlaying(false);
}
_phase++;
break;
case kLBPhaseMain:
if (!_introDone)
break;
for (uint32 i = 0; i < _items.size(); i++)
_items[i]->startPhase(_phase);
_phase++;
break;
default:
break;
}
while (_eventQueue.size()) {
DelayedEvent delayedEvent = _eventQueue.pop();
for (uint32 i = 0; i < _items.size(); i++) {
if (_items[i] != delayedEvent.item)
continue;
switch (delayedEvent.type) {
case kLBDelayedEventDestroy:
_items.remove_at(i);
i--;
_orderedItems.remove(delayedEvent.item);
_page->itemDestroyed(delayedEvent.item);
delete delayedEvent.item;
if (_focus == delayedEvent.item)
_focus = NULL;
break;
case kLBDelayedEventSetNotVisible:
_items[i]->setVisible(false);
break;
case kLBDelayedEventDone:
_items[i]->done(true);
break;
default:
break;
}
break;
}
}
for (uint16 i = 0; i < _items.size(); i++)
_items[i]->update();
if (_needsRedraw) {
for (Common::List<LBItem *>::const_iterator i = _orderedItems.reverse_begin(); i != _orderedItems.end(); --i)
(*i)->draw();
_needsRedraw = false;
_needsUpdate = true;
}
}
void MohawkEngine_LivingBooks::addArchive(Archive *archive) {
_mhk.push_back(archive);
}
void MohawkEngine_LivingBooks::removeArchive(Archive *archive) {
for (uint i = 0; i < _mhk.size(); i++) {
if (archive != _mhk[i])
continue;
_mhk.remove_at(i);
return;
}
error("removeArchive didn't find archive");
}
void MohawkEngine_LivingBooks::addItem(LBItem *item) {
_items.push_back(item);
_orderedItems.push_front(item);
item->_iterator = _orderedItems.begin();
}
void MohawkEngine_LivingBooks::removeItems(const Common::Array<LBItem *> &items) {
for (uint i = 0; i < items.size(); i++) {
bool found = false;
for (uint16 j = 0; j < _items.size(); j++) {
if (items[i] != _items[j])
continue;
found = true;
_items.remove_at(j);
break;
}
assert(found);
_orderedItems.erase(items[i]->_iterator);
}
}
LBItem *MohawkEngine_LivingBooks::getItemById(uint16 id) {
for (uint16 i = 0; i < _items.size(); i++)
if (_items[i]->getId() == id)
return _items[i];
return NULL;
}
LBItem *MohawkEngine_LivingBooks::getItemByName(Common::String name) {
for (uint16 i = 0; i < _items.size(); i++)
if (_items[i]->getName() == name)
return _items[i];
return NULL;
}
void MohawkEngine_LivingBooks::setFocus(LBItem *focus) {
_focus = focus;
}
void MohawkEngine_LivingBooks::setEnableForAll(bool enable, LBItem *except) {
for (uint16 i = 0; i < _items.size(); i++)
if (except != _items[i])
_items[i]->setEnabled(enable);
}
void MohawkEngine_LivingBooks::notifyAll(uint16 data, uint16 from) {
for (uint16 i = 0; i < _items.size(); i++)
_items[i]->notify(data, from);
}
void MohawkEngine_LivingBooks::queueDelayedEvent(DelayedEvent event) {
_eventQueue.push(event);
}
bool MohawkEngine_LivingBooks::playSound(LBItem *source, uint16 resourceId) {
if (_lastSoundId && !_sound->isPlaying(_lastSoundId))
_lastSoundId = 0;
if (!source->isAmbient() || !_sound->isPlaying()) {
if (!_soundLockOwner) {
if (_lastSoundId && _lastSoundOwner != source->getId())
if (source->getSoundPriority() >= _lastSoundPriority)
return false;
} else {
if (_soundLockOwner != source->getId() && source->getSoundPriority() >= _maxSoundPriority)
return false;
}
if (_lastSoundId)
_sound->stopSound(_lastSoundId);
_lastSoundOwner = source->getId();
_lastSoundPriority = source->getSoundPriority();
}
_lastSoundId = resourceId;
_sound->playSound(resourceId);
return true;
}
void MohawkEngine_LivingBooks::lockSound(LBItem *owner, bool lock) {
if (!lock) {
_soundLockOwner = 0;
return;
}
if (_soundLockOwner || (owner->isAmbient() && _sound->isPlaying()))
return;
if (_lastSoundId && !_sound->isPlaying(_lastSoundId))
_lastSoundId = 0;
_soundLockOwner = owner->getId();
_maxSoundPriority = owner->getSoundPriority();
if (_lastSoundId && _maxSoundPriority <= _lastSoundPriority) {
_sound->stopSound(_lastSoundId);
_lastSoundId = 0;
}
}
// Only 1 VSRN resource per page
uint16 LBPage::getResourceVersion() {
Common::SeekableReadStream *versionStream = _vm->getResource(ID_VRSN, _baseId);
// FIXME: some V2 games have very strange version entries
if (versionStream->size() != 2)
debug(1, "Version Record size mismatch");
uint16 version = versionStream->readUint16BE();
delete versionStream;
return version;
}
void LBPage::loadBITL(uint16 resourceId) {
Common::SeekableSubReadStreamEndian *bitlStream = _vm->wrapStreamEndian(ID_BITL, resourceId);
while (true) {
Common::Rect rect = _vm->readRect(bitlStream);
uint16 type = bitlStream->readUint16();
LBItem *res;
switch (type) {
case kLBPictureItem:
res = new LBPictureItem(_vm, this, rect);
break;
case kLBAnimationItem:
res = new LBAnimationItem(_vm, this, rect);
break;
case kLBPaletteItem:
res = new LBPaletteItem(_vm, this, rect);
break;
case kLBGroupItem:
res = new LBGroupItem(_vm, this, rect);
break;
case kLBSoundItem:
res = new LBSoundItem(_vm, this, rect);
break;
case kLBLiveTextItem:
res = new LBLiveTextItem(_vm, this, rect);
break;
case kLBMovieItem:
res = new LBMovieItem(_vm, this, rect);
break;
case kLBMiniGameItem:
res = new LBMiniGameItem(_vm, this, rect);
break;
case kLBProxyItem:
res = new LBProxyItem(_vm, this, rect);
break;
default:
warning("Unknown item type %04x", type);
// fall through
case 3: // often used for buttons
res = new LBItem(_vm, this, rect);
break;
}
res->readFrom(bitlStream);
_items.push_back(res);
if (bitlStream->size() == bitlStream->pos())
break;
}
delete bitlStream;
}
Common::SeekableSubReadStreamEndian *MohawkEngine_LivingBooks::wrapStreamEndian(uint32 tag, uint16 id) {
Common::SeekableReadStream *dataStream = getResource(tag, id);
return new Common::SeekableSubReadStreamEndian(dataStream, 0, dataStream->size(), isBigEndian(), DisposeAfterUse::YES);
}
Common::String MohawkEngine_LivingBooks::getStringFromConfig(const Common::String &section, const Common::String &key) {
Common::String x, leftover;
_bookInfoFile.getKey(key, section, x);
Common::String tmp = removeQuotesFromString(x, leftover);
if (!leftover.empty())
warning("while parsing config key '%s' from section '%s', string '%s' was left after '%s'",
key.c_str(), section.c_str(), leftover.c_str(), tmp.c_str());
return tmp;
}
Common::String MohawkEngine_LivingBooks::getStringFromConfig(const Common::String &section, const Common::String &key, Common::String &leftover) {
Common::String x;
_bookInfoFile.getKey(key, section, x);
return removeQuotesFromString(x, leftover);
}
int MohawkEngine_LivingBooks::getIntFromConfig(const Common::String &section, const Common::String &key) {
return atoi(getStringFromConfig(section, key).c_str());
}
Common::String MohawkEngine_LivingBooks::getFileNameFromConfig(const Common::String &section, const Common::String &key, Common::String &leftover) {
Common::String string = getStringFromConfig(section, key, leftover);
if (string.hasPrefix("//")) {
// skip "//CD-ROM Title/" prefixes which we don't care about
uint i = 3;
while (i < string.size() && string[i - 1] != '/')
i++;
// Already uses slashes, no need to convert further
return string.c_str() + i;
}
return (getPlatform() == Common::kPlatformMacintosh) ? convertMacFileName(string) : convertWinFileName(string);
}
Common::String MohawkEngine_LivingBooks::removeQuotesFromString(const Common::String &string, Common::String &leftover) {
if (string.empty())
return string;
char quoteChar = string[0];
if (quoteChar != '\"' && quoteChar != '\'')
return string;
Common::String tmp;
bool inLeftover = false;
for (uint32 i = 1; i < string.size(); i++) {
if (inLeftover)
leftover += string[i];
else if (string[i] == quoteChar)
inLeftover = true;
else
tmp += string[i];
}
return tmp;
}
Common::String MohawkEngine_LivingBooks::convertMacFileName(const Common::String &string) {
Common::String filename;
for (uint32 i = 0; i < string.size(); i++) {
if (i == 0 && string[i] == ':') // First character should be ignored (another colon)
continue;
if (string[i] == ':') // Directory separator
filename += '/';
else if (string[i] == '/') // Literal slash
filename += ':'; // Replace by colon, as used by Mac OS X for slash
else
filename += string[i];
}
return filename;
}
Common::String MohawkEngine_LivingBooks::convertWinFileName(const Common::String &string) {
Common::String filename;
for (uint32 i = 0; i < string.size(); i++) {
if (i == 0 && (string[i] == '/' || string[i] == '\\')) // ignore slashes at start
continue;
if (string[i] == '\\')
filename += '/';
else
filename += string[i];
}
return filename;
}
Archive *MohawkEngine_LivingBooks::createArchive() const {
if (isPreMohawk())
return new LivingBooksArchive_v1();
return new MohawkArchive();
}
bool MohawkEngine_LivingBooks::isPreMohawk() const {
return getGameType() == GType_LIVINGBOOKSV1
|| (getGameType() == GType_LIVINGBOOKSV2 && getPlatform() == Common::kPlatformMacintosh);
}
void MohawkEngine_LivingBooks::addNotifyEvent(NotifyEvent event) {
_notifyEvents.push(event);
}
bool MohawkEngine_LivingBooks::tryLoadPageStart(LBMode mode, uint page) {
// try first subpage of the page
if (loadPage(mode, page, 1))
return true;
// then just the plain page
if (loadPage(mode, page, 0))
return true;
return false;
}
bool MohawkEngine_LivingBooks::tryDefaultPage() {
if (_curMode == kLBCreditsMode || _curMode == kLBPreviewMode) {
// go to options page
if (getFeatures() & GF_LB_10) {
if (tryLoadPageStart(kLBControlMode, 2))
return true;
} else {
if (tryLoadPageStart(kLBControlMode, 3))
return true;
}
}
// go to menu page
if (tryLoadPageStart(kLBControlMode, 1))
return true;
return false;
}
void MohawkEngine_LivingBooks::prevPage() {
if (_curPage > 1 && (tryLoadPageStart(_curMode, _curPage - 1)))
return;
if (tryDefaultPage())
return;
error("Could not find page before %d.%d for mode %d", _curPage, _curSubPage, (int)_curMode);
}
void MohawkEngine_LivingBooks::nextPage() {
// we try the next subpage first
if (loadPage(_curMode, _curPage, _curSubPage + 1))
return;
if (tryLoadPageStart(_curMode, _curPage + 1))
return;
if (tryDefaultPage())
return;
error("Could not find page after %d.%d for mode %d", _curPage, _curSubPage, (int)_curMode);
}
void MohawkEngine_LivingBooks::handleUIMenuClick(uint controlId) {
LBItem *item;
switch (controlId) {
case 1:
if (getFeatures() & GF_LB_10) {
if (!tryLoadPageStart(kLBControlMode, 2))
error("couldn't load options page");
} else {
if (!tryLoadPageStart(kLBControlMode, 3))
error("couldn't load options page");
}
break;
case 2:
item = getItemById(10);
if (item)
item->destroySelf();
item = getItemById(11);
if (item)
item->destroySelf();
item = getItemById(199 + _curLanguage);
if (item) {
item->setVisible(true);
item->togglePlaying(false, true);
}
break;
case 3:
item = getItemById(10);
if (item)
item->destroySelf();
item = getItemById(11);
if (item)
item->destroySelf();
item = getItemById(12);
if (item) {
item->setVisible(true);
item->togglePlaying(false, true);
}
break;
case 4:
if (getFeatures() & GF_LB_10) {
if (!tryLoadPageStart(kLBControlMode, 3))
error("couldn't load quit page");
} else {
if (!tryLoadPageStart(kLBControlMode, 2))
error("couldn't load quit page");
}
break;
case 10:
item = getItemById(10);
if (item)
item->destroySelf();
item = getItemById(11);
if (item) {
item->setVisible(true);
item->togglePlaying(false);
}
break;
case 11:
item = getItemById(11);
if (item)
item->togglePlaying(false, true);
break;
case 12:
// start game, in play mode
if (!tryLoadPageStart(kLBPlayMode, 1))
error("couldn't start play mode");
break;
default:
if (controlId >= 100 && controlId < 100 + (uint)_numLanguages) {
uint newLanguage = controlId - 99;
if (newLanguage == _curLanguage)
break;
item = getItemById(99 + _curLanguage);
if (item)
item->seek(1);
_curLanguage = newLanguage;
} else if (controlId >= 200 && controlId < 200 + (uint)_numLanguages) {
// start game, in read mode
if (!tryLoadPageStart(kLBReadMode, 1))
error("couldn't start read mode");
}
break;
}
}
void MohawkEngine_LivingBooks::handleUIPoetryMenuClick(uint controlId) {
LBItem *item;
// the menu UI in New Kid on the Block is a hybrid of the normal menu
// and the normal options screen
// TODO: this is mostly untested
switch (controlId) {
case 2:
case 3:
handleUIOptionsClick(controlId);
break;
case 4:
handleUIMenuClick(controlId);
break;
case 6:
handleUIMenuClick(2);
break;
case 7:
item = getItemById(10);
if (item)
item->destroySelf();
item = getItemById(11);
if (item)
item->destroySelf();
item = getItemById(12);
if (item) {
item->setVisible(true);
item->togglePlaying(false, true);
}
break;
case 0xA:
item = getItemById(10);
if (item)
item->destroySelf();
item = getItemById(11);
if (item) {
item->setVisible(true);
item->togglePlaying(false);
}
break;
case 0xB:
item = getItemById(11);
if (item)
item->togglePlaying(false, true);
break;
case 0xC:
if (!tryLoadPageStart(kLBPlayMode, _curSelectedPage))
error("failed to load page %d", _curSelectedPage);
break;
default:
if (controlId < 100) {
handleUIMenuClick(controlId);
} else {
if (!tryLoadPageStart(kLBReadMode, _curSelectedPage))
error("failed to load page %d", _curSelectedPage);
}
}
}
void MohawkEngine_LivingBooks::handleUIQuitClick(uint controlId) {
LBItem *item;
switch (controlId) {
case 1:
case 2:
// button clicked, run animation
item = getItemById(10);
if (item)
item->destroySelf();
item = getItemById(11);
if (item)
item->destroySelf();
item = getItemById((controlId == 1) ? 12 : 13);
if (item) {
item->setVisible(true);
item->togglePlaying(false);
}
break;
case 10:
case 11:
item = getItemById(11);
if (item)
item->togglePlaying(false, true);
break;
case 12:
// 'yes', I want to quit
quitGame();
break;
case 13:
// 'no', go back to menu
if (!tryLoadPageStart(kLBControlMode, 1))
error("couldn't return to menu");
break;
default:
break;
}
}
void MohawkEngine_LivingBooks::handleUIOptionsClick(uint controlId) {
LBItem *item;
switch (controlId) {
case 1:
item = getItemById(10);
if (item)
item->destroySelf();
item = getItemById(202);
if (item) {
item->setVisible(true);
item->togglePlaying(false, true);
}
break;
case 2:
// back
item = getItemById(2);
if (item)
item->seek(1);
if (_curSelectedPage == 1) {
_curSelectedPage = _numPages;
} else {
_curSelectedPage--;
}
for (uint i = 0; i < _numPages; i++) {
item = getItemById(1000 + i);
if (item)
item->setVisible(_curSelectedPage == i + 1);
item = getItemById(1100 + i);
if (item)
item->setVisible(_curSelectedPage == i + 1);
}
break;
case 3:
// forward
item = getItemById(3);
if (item)
item->seek(1);
if (_curSelectedPage == _numPages) {
_curSelectedPage = 1;
} else {
_curSelectedPage++;
}
for (uint i = 0; i < _numPages; i++) {
item = getItemById(1000 + i);
if (item)
item->setVisible(_curSelectedPage == i + 1);
item = getItemById(1100 + i);
if (item)
item->setVisible(_curSelectedPage == i + 1);
}
break;
case 4:
if (!tryLoadPageStart(kLBCreditsMode, 1))
error("failed to start credits");
break;
case 5:
if (!tryLoadPageStart(kLBPreviewMode, 1))
error("failed to start preview");
break;
case 202:
if (!tryLoadPageStart(kLBPlayMode, _curSelectedPage))
error("failed to load page %d", _curSelectedPage);
break;
default:
break;
}
}
void MohawkEngine_LivingBooks::handleNotify(NotifyEvent &event) {
// hard-coded behavior (GUI/navigation)
switch (event.type) {
case kLBNotifyGUIAction:
debug(2, "kLBNotifyGUIAction: %d", event.param);
if (_curMode != kLBControlMode)
break;
// The scripting passes us the control ID as param, so we work
// out which control was clicked, then run the relevant code.
uint16 page;
page = _curPage;
if (getFeatures() & GF_LB_10) {
// Living Books 1.0 had the meanings of these pages reversed
if (page == 2)
page = 3;
else if (page == 3)
page = 2;
}
switch (page) {
case 1:
// main menu
if (_poetryMode)
handleUIPoetryMenuClick(event.param);
else
handleUIMenuClick(event.param);
break;
case 2:
// quit screen
handleUIQuitClick(event.param);
break;
case 3:
// options screen
handleUIOptionsClick(event.param);
break;
default:
break;
}
break;
case kLBNotifyGoToControls:
debug(2, "kLBNotifyGoToControls: %d", event.param);
if (!tryLoadPageStart(kLBControlMode, 1))
error("couldn't load controls page");
break;
case kLBNotifyChangePage:
switch (event.param) {
case 0xfffe:
debug(2, "kLBNotifyChangePage: next page");
nextPage();
return;
case 0xffff:
debug(2, "kLBNotifyChangePage: previous page");
prevPage();
break;
default:
debug(2, "kLBNotifyChangePage: trying %d", event.param);
if (!tryLoadPageStart(_curMode, event.param)) {
if (!tryDefaultPage()) {
error("failed to load default page after change to page %d (mode %d) failed", event.param, _curMode);
}
}
break;
}
break;
case kLBNotifyGotoQuit:
debug(2, "kLBNotifyGotoQuit: %d", event.param);
if (!tryLoadPageStart(kLBControlMode, 2))
error("couldn't load quit page");
break;
case kLBNotifyIntroDone:
debug(2, "kLBNotifyIntroDone: %d", event.param);
if (event.param != 1)
break;
_introDone = true;
// TODO: if !_readOnly, go to next page (-2 case above)
// if in older one (not in e.g. 1.4 w/tortoise),
// if mode is 6 (kLBPlayMode?), go to next page (-2 case) if curr page > nPages (i.e. the end)
// else, nothing
if (!_readOnly)
break;
nextPage();
break;
case kLBNotifyChangeMode:
if (getGameType() == GType_LIVINGBOOKSV1) {
debug(2, "kLBNotifyChangeMode: %d", event.param);
quitGame();
break;
}
debug(2, "kLBNotifyChangeMode: v2 type %d", event.param);
switch (event.param) {
case 1:
debug(2, "kLBNotifyChangeMode:, mode %d, page %d.%d",
event.newMode, event.newPage, event.newSubpage);
// TODO: what is entry.newUnknown?
if (!event.newMode)
event.newMode = _curMode;
if (!loadPage((LBMode)event.newMode, event.newPage, event.newSubpage)) {
if (event.newPage != 0 || !loadPage((LBMode)event.newMode, _curPage, event.newSubpage))
if (event.newSubpage != 0 || !loadPage((LBMode)event.newMode, event.newPage, 1))
if (event.newSubpage != 1 || !loadPage((LBMode)event.newMode, event.newPage, 0))
error("kLBNotifyChangeMode failed to move to mode %d, page %d.%d",
event.newMode, event.newPage, event.newSubpage);
}
break;
case 3:
debug(2, "kLBNotifyChangeMode: new cursor '%s'", event.newCursor.c_str());
_cursor->setCursor(event.newCursor);
break;
default:
error("unknown v2 kLBNotifyChangeMode type %d", event.param);
}
break;
case kLBNotifyCursorChange:
debug(2, "kLBNotifyCursorChange: %d", event.param);
// TODO: show/hide cursor according to parameter?
break;
case kLBNotifyPrintPage:
debug(2, "kLBNotifyPrintPage: %d", event.param);
warning("kLBNotifyPrintPage unimplemented");
break;
case kLBNotifyQuit:
debug(2, "kLBNotifyQuit: %d", event.param);
quitGame();
break;
default:
error("Unknown notification %d (param 0x%04x)", event.type, event.param);
}
}
LBAnimationNode::LBAnimationNode(MohawkEngine_LivingBooks *vm, LBAnimation *parent, uint16 scriptResourceId) : _vm(vm), _parent(parent) {
loadScript(scriptResourceId);
}
LBAnimationNode::~LBAnimationNode() {
for (uint32 i = 0; i < _scriptEntries.size(); i++)
delete[] _scriptEntries[i].data;
}
void LBAnimationNode::loadScript(uint16 resourceId) {
Common::SeekableSubReadStreamEndian *scriptStream = _vm->wrapStreamEndian(ID_SCRP, resourceId);
reset();
while (byte opcodeId = scriptStream->readByte()) {
byte size = scriptStream->readByte();
LBAnimScriptEntry entry;
entry.opcode = opcodeId;
entry.size = size;
if (!size) {
entry.data = NULL;
} else {
entry.data = new byte[entry.size];
scriptStream->read(entry.data, entry.size);
}
_scriptEntries.push_back(entry);
}
byte size = scriptStream->readByte();
if (size != 0 || scriptStream->pos() != scriptStream->size())
error("Failed to read script correctly");
delete scriptStream;
}
void LBAnimationNode::draw(const Common::Rect &_bounds) {
if (!_currentCel)
return;
// this is also checked in SetCel, below
if (_currentCel > _parent->getNumResources())
error("Animation cel %d was too high, this shouldn't happen!", _currentCel);
int16 xOffset = _xPos + _bounds.left;
int16 yOffset = _yPos + _bounds.top;
uint16 resourceId = _parent->getResource(_currentCel - 1);
if (!_vm->isPreMohawk()) {
Common::Point offset = _parent->getOffset(_currentCel - 1);
xOffset -= offset.x;
yOffset -= offset.y;
}
_vm->_gfx->copyOffsetAnimImageToScreen(resourceId, xOffset, yOffset);
}
void LBAnimationNode::reset() {
// TODO: this causes stupid flickering
//if (_currentCel)
// _vm->_needsRedraw = true;
_currentCel = 0;
_currentEntry = 0;
_delay = 0;
_xPos = 0;
_yPos = 0;
}
NodeState LBAnimationNode::update(bool seeking) {
if (_currentEntry == _scriptEntries.size())
return kLBNodeDone;
if (_delay > 0 && --_delay)
return kLBNodeRunning;
while (_currentEntry < _scriptEntries.size()) {
LBAnimScriptEntry &entry = _scriptEntries[_currentEntry];
_currentEntry++;
debug(5, "Running script entry %d of %d", _currentEntry, _scriptEntries.size());
switch (entry.opcode) {
case kLBAnimOpPlaySound:
case kLBAnimOpWaitForSound:
case kLBAnimOpReleaseSound:
case kLBAnimOpResetSound:
{
uint16 soundResourceId = READ_BE_UINT16(entry.data);
if (!soundResourceId) {
error("Unhandled named wave file, tell clone2727 where you found this");
break;
}
Common::String cue;
uint pos = 2;
while (pos < entry.size) {
char in = entry.data[pos];
if (!in)
break;
pos++;
cue += in;
}
if (pos == entry.size)
error("Cue in sound kLBAnimOp wasn't null-terminated");
switch (entry.opcode) {
case kLBAnimOpPlaySound:
if (seeking)
break;
debug(4, "a: PlaySound(%0d)", soundResourceId);
_parent->playSound(soundResourceId);
break;
case kLBAnimOpWaitForSound:
if (seeking)
break;
debug(4, "b: WaitForSound(%0d)", soundResourceId);
if (!_parent->soundPlaying(soundResourceId, cue))
break;
_currentEntry--;
return kLBNodeWaiting;
case kLBAnimOpReleaseSound:
debug(4, "c: ReleaseSound(%0d)", soundResourceId);
// TODO
_vm->_sound->stopSound(soundResourceId);
break;
case kLBAnimOpResetSound:
debug(4, "d: ResetSound(%0d)", soundResourceId);
// TODO
_vm->_sound->stopSound(soundResourceId);
break;
default:
break;
}
}
break;
case kLBAnimOpSetTempo:
case kLBAnimOpSetTempoDiv:
{
assert(entry.size == 2);
uint16 tempo = (int16)READ_BE_UINT16(entry.data);
// TODO: LB 3 uses fixed-point here.
if (entry.opcode == kLBAnimOpSetTempo) {
debug(4, "3: SetTempo(%d)", tempo);
// TODO: LB 3 uses (tempo * 1000) / 60, while
// the original divides the system time by 16.
_parent->setTempo(tempo * 16);
} else {
// LB 3.0+ only.
debug(4, "E: SetTempoDiv(%d)", tempo);
_parent->setTempo(1000 / tempo);
}
}
break;
case kLBAnimOpWait:
assert(entry.size == 0);
debug(5, "6: Wait()");
return kLBNodeRunning;
case kLBAnimOpMoveTo:
{
assert(entry.size == 4);
int16 x = (int16)READ_BE_UINT16(entry.data);
int16 y = (int16)READ_BE_UINT16(entry.data + 2);
debug(4, "5: MoveTo(%d, %d)", x, y);
_xPos = x;
_yPos = y;
_vm->_needsRedraw = true;
}
break;
case kLBAnimOpDrawMode:
{
assert(entry.size == 2);
uint16 mode = (int16)READ_BE_UINT16(entry.data);
debug(4, "9: DrawMode(%d)", mode);
// TODO
}
break;
case kLBAnimOpSetCel:
{
assert(entry.size == 2);
uint16 cel = (int16)READ_BE_UINT16(entry.data);
debug(4, "7: SetCel(%d)", cel);
_currentCel = cel;
if (_currentCel > _parent->getNumResources())
error("SetCel set current cel to %d, but we only have %d cels", _currentCel, _parent->getNumResources());
_vm->_needsRedraw = true;
}
break;
case kLBAnimOpNotify:
{
assert(entry.size == 2);
uint16 data = (int16)READ_BE_UINT16(entry.data);
if (seeking)
break;
debug(4, "2: Notify(%d)", data);
_vm->notifyAll(data, _parent->getParentId());
}
break;
case kLBAnimOpSleepUntil:
{
assert(entry.size == 4);
uint32 frame = READ_BE_UINT32(entry.data);
debug(4, "8: SleepUntil(%d)", frame);
if (frame > _parent->getCurrentFrame()) {
// *not* kLBNodeWaiting
_currentEntry--;
return kLBNodeRunning;
}
}
break;
case kLBAnimOpDelay:
{
assert(entry.size == 4);
uint32 delay = READ_BE_UINT32(entry.data);
debug(4, "f: Delay(%d)", delay);
_delay = delay;
return kLBNodeRunning;
}
break;
default:
error("Unknown opcode id %02x (size %d)", entry.opcode, entry.size);
break;
}
}
return kLBNodeRunning;
}
bool LBAnimationNode::transparentAt(int x, int y) {
if (!_currentCel)
return true;
uint16 resourceId = _parent->getResource(_currentCel - 1);
if (!_vm->isPreMohawk()) {
Common::Point offset = _parent->getOffset(_currentCel - 1);
x += offset.x;
y += offset.y;
}
// TODO: only check pixels if necessary
return _vm->_gfx->imageIsTransparentAt(resourceId, true, x - _xPos, y - _yPos);
}
LBAnimation::LBAnimation(MohawkEngine_LivingBooks *vm, LBAnimationItem *parent, uint16 resourceId) : _vm(vm), _parent(parent) {
Common::SeekableSubReadStreamEndian *aniStream = _vm->wrapStreamEndian(ID_ANI, resourceId);
if (aniStream->size() != 30)
warning("ANI Record size mismatch");
uint16 version = aniStream->readUint16();
if (version != 1)
warning("ANI version not 1");
_bounds = _vm->readRect(aniStream);
_clip = _vm->readRect(aniStream);
// TODO: what is colorId for?
uint32 colorId = aniStream->readUint32();
uint32 sprResourceId = aniStream->readUint32();
uint32 sprResourceOffset = aniStream->readUint32();
debug(5, "ANI bounds: (%d, %d), (%d, %d)", _bounds.left, _bounds.top, _bounds.right, _bounds.bottom);
debug(5, "ANI clip: (%d, %d), (%d, %d)", _clip.left, _clip.top, _clip.right, _clip.bottom);
debug(5, "ANI color id: %d", colorId);
debug(5, "ANI SPRResourceId: %d, offset %d", sprResourceId, sprResourceOffset);
if (aniStream->pos() != aniStream->size())
error("Still %d bytes at the end of anim stream", aniStream->size() - aniStream->pos());
delete aniStream;
if (sprResourceOffset)
error("Cannot handle non-zero ANI offset yet");
Common::SeekableSubReadStreamEndian *sprStream = _vm->wrapStreamEndian(ID_SPR, sprResourceId);
uint16 numBackNodes = sprStream->readUint16();
uint16 numFrontNodes = sprStream->readUint16();
uint32 shapeResourceID = sprStream->readUint32();
uint32 shapeResourceOffset = sprStream->readUint32();
uint32 scriptResourceID = sprStream->readUint32();
uint32 scriptResourceOffset = sprStream->readUint32();
uint32 scriptResourceLength = sprStream->readUint32();
debug(5, "SPR# stream: %d front, %d background", numFrontNodes, numBackNodes);
debug(5, "Shape ID %d (offset 0x%04x), script ID %d (offset 0x%04x, length %d)", shapeResourceID, shapeResourceOffset,
scriptResourceID, scriptResourceOffset, scriptResourceLength);
Common::Array<uint16> scriptIDs;
for (uint16 i = 0; i < numFrontNodes; i++) {
uint32 unknown1 = sprStream->readUint32();
uint32 unknown2 = sprStream->readUint32();
uint32 unknown3 = sprStream->readUint32();
uint16 scriptID = sprStream->readUint32();
uint32 unknown4 = sprStream->readUint32();
uint32 unknown5 = sprStream->readUint32();
scriptIDs.push_back(scriptID);
debug(6, "Front node %d: script ID %d", i, scriptID);
if (unknown1 != 0 || unknown2 != 0 || unknown3 != 0 || unknown4 != 0 || unknown5 != 0)
error("Anim node %d had non-zero unknowns %08x, %08x, %08x, %08x, %08x",
i, unknown1, unknown2, unknown3, unknown4, unknown5);
}
if (numBackNodes)
error("Ignoring %d back nodes", numBackNodes);
if (sprStream->pos() != sprStream->size())
error("Still %d bytes at the end of sprite stream", sprStream->size() - sprStream->pos());
delete sprStream;
loadShape(shapeResourceID);
_nodes.push_back(new LBAnimationNode(_vm, this, scriptResourceID));
for (uint16 i = 0; i < scriptIDs.size(); i++)
_nodes.push_back(new LBAnimationNode(_vm, this, scriptIDs[i]));
_currentFrame = 0;
_currentSound = 0xffff;
_running = false;
_tempo = 1;
}
LBAnimation::~LBAnimation() {
for (uint32 i = 0; i < _nodes.size(); i++)
delete _nodes[i];
if (_currentSound != 0xffff)
_vm->_sound->stopSound(_currentSound);
}
void LBAnimation::loadShape(uint16 resourceId) {
if (resourceId == 0)
return;
Common::SeekableSubReadStreamEndian *shapeStream = _vm->wrapStreamEndian(ID_SHP, resourceId);
if (_vm->isPreMohawk()) {
if (shapeStream->size() < 6)
error("V1 SHP Record size too short (%d)", shapeStream->size());
uint16 u0 = shapeStream->readUint16();
if (u0 != 3)
error("V1 SHP Record u0 is %04x, not 3", u0);
uint16 u1 = shapeStream->readUint16();
if (u1 != 0)
error("V1 SHP Record u1 is %04x, not 0", u1);
uint16 idCount = shapeStream->readUint16();
debug(8, "V1 SHP: idCount: %d", idCount);
if (shapeStream->size() != (idCount * 2) + 6)
error("V1 SHP Record size mismatch (%d)", shapeStream->size());
for (uint16 i = 0; i < idCount; i++) {
_shapeResources.push_back(shapeStream->readUint16());
debug(8, "V1 SHP: BMAP Resource Id %d: %d", i, _shapeResources[i]);
}
} else {
uint16 idCount = shapeStream->readUint16();
debug(8, "SHP: idCount: %d", idCount);
if (shapeStream->size() != (idCount * 6) + 2)
error("SHP Record size mismatch (%d)", shapeStream->size());
for (uint16 i = 0; i < idCount; i++) {
_shapeResources.push_back(shapeStream->readUint16());
int16 x = shapeStream->readSint16();
int16 y = shapeStream->readSint16();
_shapeOffsets.push_back(Common::Point(x, y));
debug(8, "SHP: tBMP Resource Id %d: %d, at (%d, %d)", i, _shapeResources[i], x, y);
}
}
for (uint16 i = 0; i < _shapeResources.size(); i++)
_vm->_gfx->preloadImage(_shapeResources[i]);
delete shapeStream;
}
void LBAnimation::draw() {
for (uint32 i = 0; i < _nodes.size(); i++)
_nodes[i]->draw(_bounds);
}
bool LBAnimation::update() {
if (!_running)
return false;
if (_vm->_system->getMillis() <= _lastTime + (uint32)_tempo)
return false;
// the second check is to try 'catching up' with lagged animations, might be crazy
if (_lastTime == 0 || (_vm->_system->getMillis()) > _lastTime + (uint32)(_tempo * 2))
_lastTime = _vm->_system->getMillis();
else
_lastTime += _tempo;
if (_currentSound != 0xffff && !_vm->_sound->isPlaying(_currentSound)) {
_currentSound = 0xffff;
}
NodeState state = kLBNodeDone;
for (uint32 i = 0; i < _nodes.size(); i++) {
NodeState s = _nodes[i]->update();
if (s == kLBNodeWaiting) {
state = kLBNodeWaiting;
if (i != 0)
warning("non-primary node was waiting");
break;
}
if (s == kLBNodeRunning)
state = kLBNodeRunning;
}
if (state == kLBNodeRunning) {
_currentFrame++;
} else if (state == kLBNodeDone) {
if (_currentSound == 0xffff) {
_running = false;
return true;
}
}
return false;
}
void LBAnimation::start() {
_lastTime = 0;
_running = true;
}
void LBAnimation::seek(uint16 pos) {
_lastTime = 0;
_currentFrame = 0;
if (_currentSound != 0xffff) {
_vm->_sound->stopSound(_currentSound);
_currentSound = 0xffff;
}
for (uint32 i = 0; i < _nodes.size(); i++)
_nodes[i]->reset();
for (uint16 n = 0; n < pos; n++) {
bool ranSomething = false;
// nodes don't wait while seeking
for (uint32 i = 0; i < _nodes.size(); i++)
ranSomething |= (_nodes[i]->update(true) != kLBNodeDone);
_currentFrame++;
if (!ranSomething) {
_running = false;
break;
}
}
}
void LBAnimation::seekToTime(uint32 time) {
_lastTime = 0;
_currentFrame = 0;
if (_currentSound != 0xffff) {
_vm->_sound->stopSound(_currentSound);
_currentSound = 0xffff;
}
for (uint32 i = 0; i < _nodes.size(); i++)
_nodes[i]->reset();
uint32 elapsed = 0;
while (elapsed <= time) {
bool ranSomething = false;
// nodes don't wait while seeking
for (uint32 i = 0; i < _nodes.size(); i++)
ranSomething |= (_nodes[i]->update(true) != kLBNodeDone);
elapsed += _tempo;
_currentFrame++;
if (!ranSomething) {
_running = false;
break;
}
}
}
void LBAnimation::stop() {
_running = false;
if (_currentSound != 0xffff) {
_vm->_sound->stopSound(_currentSound);
_currentSound = 0xffff;
}
}
void LBAnimation::playSound(uint16 resourceId) {
_currentSound = resourceId;
_vm->_sound->playSound(_currentSound, Audio::Mixer::kMaxChannelVolume, false, &_cueList);
}
bool LBAnimation::soundPlaying(uint16 resourceId, const Common::String &cue) {
if (_currentSound != resourceId)
return false;
if (!_vm->_sound->isPlaying(_currentSound))
return false;
if (cue.empty())
return true;
uint samples = _vm->_sound->getNumSamplesPlayed(_currentSound);
for (uint i = 0; i < _cueList.pointCount; i++) {
if (_cueList.points[i].sampleFrame > samples)
break;
if (_cueList.points[i].name == cue)
return false;
}
return true;
}
bool LBAnimation::transparentAt(int x, int y) {
for (uint32 i = 0; i < _nodes.size(); i++)
if (!_nodes[i]->transparentAt(x - _bounds.left, y - _bounds.top))
return false;
return true;
}
void LBAnimation::setTempo(uint16 tempo) {
_tempo = tempo;
}
uint16 LBAnimation::getParentId() {
return _parent->getId();
}
LBScriptEntry::LBScriptEntry() {
state = 0;
data = NULL;
argvParam = NULL;
argvTarget = NULL;
}
LBScriptEntry::~LBScriptEntry() {
delete[] argvParam;
delete[] argvTarget;
delete[] data;
for (uint i = 0; i < subentries.size(); i++)
delete subentries[i];
}
LBItem::LBItem(MohawkEngine_LivingBooks *vm, LBPage *page, Common::Rect rect) : _vm(vm), _page(page), _rect(rect) {
if (_vm->getGameType() == GType_LIVINGBOOKSV1 || _vm->getGameType() == GType_LIVINGBOOKSV2)
_phase = kLBPhaseInit;
else
_phase = kLBPhaseLoad;
_loopMode = 0;
_delayMin = 0;
_delayMax = 0;
_timingMode = kLBAutoNone;
_periodMin = 0;
_periodMax = 0;
_controlMode = kLBControlNone;
_soundMode = 0;
_loaded = false;
_enabled = false;
_visible = true;
_playing = false;
_globalEnabled = true;
_globalVisible = true;
_nextTime = 0;
_startTime = 0;
_loops = 0;
_isAmbient = false;
_doHitTest = true;
}
LBItem::~LBItem() {
for (uint i = 0; i < _scriptEntries.size(); i++)
delete _scriptEntries[i];
}
void LBItem::readFrom(Common::SeekableSubReadStreamEndian *stream) {
_resourceId = stream->readUint16();
_itemId = stream->readUint16();
uint16 size = stream->readUint16();
_desc = _vm->readString(stream);
debug(2, "Item: size %d, resource %d, id %d", size, _resourceId, _itemId);
debug(2, "Coords: %d, %d, %d, %d", _rect.left, _rect.top, _rect.right, _rect.bottom);
debug(2, "String: '%s'", _desc.c_str());
if (!_itemId)
error("Item had invalid item id");
int endPos = stream->pos() + size;
if (endPos > stream->size())
error("Item is larger (should end at %d) than stream (size %d)", endPos, stream->size());
while (true) {
if (stream->pos() == endPos)
break;
uint oldPos = stream->pos();
uint16 dataType = stream->readUint16();
uint16 dataSize = stream->readUint16();
debug(4, "Data type %04x, size %d", dataType, dataSize);
byte *buf = new byte[dataSize];
stream->read(buf, dataSize);
readData(dataType, dataSize, buf);
delete[] buf;
if ((uint)stream->pos() != oldPos + 4 + (uint)dataSize)
error("Failed to read correct number of bytes (off by %d) for data type %04x (size %d)",
(int)stream->pos() - (int)(oldPos + 4 + (uint)dataSize), dataType, dataSize);
if (stream->pos() > endPos)
error("Read off the end (at %d) of data (ends at %d)", stream->pos(), endPos);
assert(!stream->eos());
}
}
LBScriptEntry *LBItem::parseScriptEntry(uint16 type, uint16 &size, Common::MemoryReadStreamEndian *stream, bool isSubentry) {
if (size < 6)
error("Script entry of type 0x%04x was too small (%d)", type, size);
uint16 expectedEndSize = 0;
LBScriptEntry *entry = new LBScriptEntry;
entry->type = type;
if (isSubentry) {
expectedEndSize = size - (stream->readUint16() + 2);
entry->event = 0xffff;
} else
entry->event = stream->readUint16();
entry->opcode = stream->readUint16();
entry->param = stream->readUint16();
debug(4, "Script entry: type 0x%04x, event 0x%04x, opcode 0x%04x, param 0x%04x",
entry->type, entry->event, entry->opcode, entry->param);
size -= 6;
// TODO: read as bytes, if this is correct (but beware endianism)
byte conditionTag = (entry->event & 0xff00) >> 8;
entry->event = entry->event & 0xff;
if (type == kLBMsgListScript && entry->opcode == kLBOpRunSubentries) {
debug(4, "%d script subentries:", entry->param);
entry->argc = 0;
for (uint i = 0; i < entry->param; i++) {
LBScriptEntry *subentry = parseScriptEntry(type, size, stream, true);
entry->subentries.push_back(subentry);
// subentries are aligned
if (i + 1 < entry->param && size % 2 == 1) {
stream->skip(1);
size--;
}
}
} else if (type == kLBMsgListScript) {
if (size < 2)
error("Script entry of type 0x%04x was too small (%d)", type, size);
entry->argc = stream->readUint16();
size -= 2;
entry->targetingType = 0;
uint16 targetingType = entry->argc;
if (targetingType == kTargetTypeExpression || targetingType == kTargetTypeCode
|| targetingType == kTargetTypeName) {
entry->targetingType = targetingType;
// FIXME
if (targetingType == kTargetTypeCode)
error("encountered kTargetTypeCode");
if (size < 2)
error("not enough bytes (%d) reading special targeting", size);
uint16 count = stream->readUint16();
size -= 2;
debug(4, "%d targets with targeting type %04x", count, targetingType);
uint oldAlign = size % 2;
for (uint i = 0; i < count; i++) {
Common::String target = _vm->readString(stream);
debug(4, "target '%s'", target.c_str());
entry->targets.push_back(target);
if (target.size() + 1 > size)
error("failed to read target (ran out of stream)");
size -= target.size() + 1;
}
entry->argc = entry->targets.size();
if ((uint)(size % 2) != oldAlign) {
stream->skip(1);
size--;
}
} else if (entry->argc) {
entry->argvParam = new uint16[entry->argc];
entry->argvTarget = new uint16[entry->argc];
debug(4, "With %d targets:", entry->argc);
if (size < (entry->argc * 4))
error("Script entry of type 0x%04x was too small (%d)", type, size);
for (uint i = 0; i < entry->argc; i++) {
entry->argvParam[i] = stream->readUint16();
entry->argvTarget[i] = stream->readUint16();
debug(4, "Target %d, param 0x%04x", entry->argvTarget[i], entry->argvParam[i]);
}
size -= (entry->argc * 4);
}
}
if (type == kLBMsgListScript && entry->opcode == kLBOpJumpUnlessExpression) {
if (size < 6)
error("not enough bytes (%d) in kLBOpJumpUnlessExpression, event 0x%04x", size, entry->event);
entry->offset = stream->readUint32();
entry->target = stream->readUint16();
debug(4, "kLBOpJumpUnlessExpression: offset %08x, target %d", entry->offset, entry->target);
size -= 6;
}
if (type == kLBMsgListScript && entry->opcode == kLBOpJumpToExpression) {
if (size < 4)
error("not enough bytes (%d) in kLBOpJumpToExpression, event 0x%04x", size, entry->event);
entry->offset = stream->readUint32();
debug(4, "kLBOpJumpToExpression: offset %08x", entry->offset);
size -= 4;
}
if (type == kLBNotifyScript && entry->opcode == kLBNotifyChangeMode && _vm->getGameType() != GType_LIVINGBOOKSV1) {
switch (entry->param) {
case 1:
if (size < 8)
error("%d unknown bytes in notify entry kLBNotifyChangeMode", size);
entry->newUnknown = stream->readUint16();
entry->newMode = stream->readUint16();
entry->newPage = stream->readUint16();
entry->newSubpage = stream->readUint16();
debug(4, "kLBNotifyChangeMode: unknown %04x, mode %d, page %d.%d",
entry->newUnknown, entry->newMode, entry->newPage, entry->newSubpage);
size -= 8;
break;
case 3:
{
Common::String newCursor = _vm->readString(stream);
entry->newCursor = newCursor;
if (size < newCursor.size() + 1)
error("failed to read newCursor in notify entry");
size -= newCursor.size() + 1;
debug(4, "kLBNotifyChangeMode: new cursor '%s'", newCursor.c_str());
}
break;
default:
// the original engine also does something when param==2 (but not a notify)
error("unknown v2 kLBNotifyChangeMode type %d", entry->param);
}
}
if (entry->opcode == kLBOpSendExpression) {
if (size < 4)
error("not enough bytes (%d) in kLBOpSendExpression, event 0x%04x", size, entry->event);
entry->offset = stream->readUint32();
debug(4, "kLBOpSendExpression: offset %08x", entry->offset);
size -= 4;
}
if (entry->opcode == kLBOpRunData) {
if (size < 4)
error("didn't get enough bytes (%d) to read data header in script entry", size);
entry->dataType = stream->readUint16();
entry->dataLen = stream->readUint16();
size -= 4;
if (size < entry->dataLen)
error("didn't get enough bytes (%d) to read data in script entry", size);
if (entry->dataType == kLBCommand) {
Common::String command = _vm->readString(stream);
uint commandSize = command.size() + 1;
if (commandSize > entry->dataLen)
error("failed to read command in script entry: dataLen %d, command '%s' (%d chars)",
entry->dataLen, command.c_str(), commandSize);
entry->dataLen = commandSize;
entry->data = new byte[commandSize];
memcpy(entry->data, command.c_str(), commandSize);
size -= commandSize;
} else {
if (conditionTag)
error("kLBOpRunData had unexpected conditionTag");
entry->data = new byte[entry->dataLen];
stream->read(entry->data, entry->dataLen);
size -= entry->dataLen;
}
}
if (entry->event == kLBEventNotified) {
if (size < 4)
error("not enough bytes (%d) in kLBEventNotified, opcode 0x%04x", size, entry->opcode);
entry->matchFrom = stream->readUint16();
entry->matchNotify = stream->readUint16();
debug(4, "kLBEventNotified: matches %04x (from %04x)",
entry->matchNotify, entry->matchFrom);
size -= 4;
}
if (isSubentry) {
// TODO: subentries may be aligned, so this check is a bit too relaxed
if (size != expectedEndSize && size != expectedEndSize + 1)
error("expected %d bytes left at end of subentry, but had %d",
expectedEndSize, size);
return entry;
}
if (conditionTag == 1) {
if (!size)
error("failed to read condition (empty stream)");
Common::String condition = _vm->readString(stream);
if (condition.size() == 0) {
size--;
if (!size)
error("failed to read condition (null byte, then ran out of stream)");
condition = _vm->readString(stream);
}
if (condition.size() + 1 > size)
error("failed to read condition (ran out of stream)");
size -= (condition.size() + 1);
entry->conditions.push_back(condition);
debug(4, "script entry condition '%s'", condition.c_str());
} else if (conditionTag == 2) {
if (size < 4)
error("expected more than %d bytes for conditionTag 2", size);
// FIXME
stream->skip(4);
size -= 4;
}
if (size == 1) {
// FIXME: this is alignment, but why?
stream->skip(1);
size--;
} else if (size)
error("failed to read script entry correctly (%d bytes left): type 0x%04x, event 0x%04x, opcode 0x%04x, param 0x%04x",
size, entry->type, entry->event, entry->opcode, entry->param);
return entry;
}
void LBItem::readData(uint16 type, uint16 size, byte *data) {
Common::MemoryReadStreamEndian stream(data, size, _vm->isBigEndian());
readData(type, size, &stream);
}
void LBItem::readData(uint16 type, uint16 size, Common::MemoryReadStreamEndian *stream) {
switch (type) {
case kLBMsgListScript:
case kLBNotifyScript:
_scriptEntries.push_back(parseScriptEntry(type, size, stream));
break;
case kLBSetPlayInfo:
{
if (size != 20)
error("kLBSetPlayInfo had wrong size (%d)", size);
_loopMode = stream->readUint16();
_delayMin = stream->readUint16();
_delayMax = stream->readUint16();
_timingMode = stream->readUint16();
if (_timingMode > 7)
error("encountered timing mode %04x", _timingMode);
_periodMin = stream->readUint16();
_periodMax = stream->readUint16();
_relocPoint.x = stream->readSint16();
_relocPoint.y = stream->readSint16();
_controlMode = stream->readUint16();
_soundMode = stream->readUint16();
debug(2, "kLBSetPlayInfo: loop mode %d (%d to %d), timing mode %d (%d to %d), reloc (%d, %d), control mode %04x, sound mode %04x",
_loopMode, _delayMin, _delayMax,
_timingMode, _periodMin, _periodMax,
_relocPoint.x, _relocPoint.y,
_controlMode, _soundMode);
}
break;
case kLBSetPlayPhase:
if (size != 2)
error("SetPlayPhase had wrong size (%d)", size);
_phase = stream->readUint16();
debug(2, "kLBSetPlayPhase: %d", _phase);
break;
case kLBSetKeyNotify:
{
// FIXME: variable-size notifies, targets
if (size != 18)
error("0x6f had wrong size (%d)", size);
uint event = stream->readUint16();
LBKey key;
stream->read(&key, 4);
uint opcode = stream->readUint16();
uint param = stream->readUint16();
uint u6 = stream->readUint16();
uint u7 = stream->readUint16();
uint u8 = stream->readUint16();
uint u9 = stream->readUint16();
warning("ignoring kLBSetKeyNotify: item %s, key code %02x (modifier mask %d, char %d, repeat %d), event %04x, opcode %04x, param %04x, unknowns %04x, %04x, %04x, %04x",
_desc.c_str(), key.code, key.modifiers, key.char_, key.repeats, event, opcode, param, u6, u7, u8, u9);
}
break;
case kLBCommand:
{
Common::String command = _vm->readString(stream);
if (size != command.size() + 1)
error("failed to read command string");
runCommand(command);
}
break;
case kLBSetNotVisible:
assert(size == 0);
_visible = false;
break;
case kLBGlobalDisable:
assert(size == 0);
_globalEnabled = false;
break;
case kLBGlobalSetNotVisible:
assert(size == 0);
_globalVisible = false;
break;
case kLBSetAmbient:
assert(size == 0);
_isAmbient = true;
break;
case kLBSetKeyEvent:
{
// FIXME: targets
if (size != 10)
error("kLBSetKeyEvent had wrong size (%d)", size);
uint u3 = stream->readUint16();
LBKey key;
stream->read(&key, 4);
uint target = stream->readUint16();
uint16 event = stream->readUint16();
// FIXME: this is scripting stuff: what to run when key is pressed
warning("ignoring kLBSetKeyEvent: item %s, key code %02x (modifier mask %d, char %d, repeat %d) unknown %04x, target %d, event %04x",
_desc.c_str(), key.code, key.modifiers, key.char_, key.repeats, u3, target, event);
}
break;
case kLBSetHitTest:
{
assert(size == 2);
uint val = stream->readUint16();
_doHitTest = (bool)val;
debug(2, "kLBSetHitTest (on %s): value %04x", _desc.c_str(), val);
}
break;
case kLBSetRolloverData:
{
assert(size == 2);
uint16 flag = stream->readUint16();
warning("ignoring kLBSetRolloverData: item %s, flag %d", _desc.c_str(), flag);
}
break;
case kLBSetParent:
{
assert(size == 2);
uint16 parent = stream->readUint16();
warning("ignoring kLBSetParent: item %s, parent id %d", _desc.c_str(), parent);
}
break;
case kLBUnknown194:
{
assert(size == 4);
uint offset = stream->readUint32();
_page->_code->runCode(this, offset);
}
break;
default:
error("Unknown message %04x (size 0x%04x)", type, size);
//for (uint i = 0; i < size; i++)
// debugN("%02x ", stream->readByte());
//debugN("\n");
break;
}
}
void LBItem::destroySelf() {
if (!this->_itemId)
error("destroySelf() on an item which was already dead");
_vm->queueDelayedEvent(DelayedEvent(this, kLBDelayedEventDestroy));
_itemId = 0;
}
void LBItem::setEnabled(bool enabled) {
if (enabled && !_loaded && !_playing) {
if (_timingMode == kLBAutoUserIdle) {
setNextTime(_periodMin, _periodMax);
debug(2, "Enable time startup");
}
}
_enabled = enabled;
}
void LBItem::setGlobalEnabled(bool enabled) {
bool wasEnabled = _loaded && _enabled && _globalEnabled;
_globalEnabled = enabled;
if (wasEnabled != (_loaded && _enabled && _globalEnabled))
setEnabled(enabled);
}
bool LBItem::contains(Common::Point point) {
if (!_loaded)
return false;
if (_playing && _loopMode == 0xFFFF)
stop();
if (!_playing && _timingMode == kLBAutoUserIdle)
setNextTime(_periodMin, _periodMax);
return _visible && _globalVisible && _rect.contains(point);
}
void LBItem::update() {
if (_phase != kLBPhaseNone && (!_loaded || !_enabled || !_globalEnabled))
return;
if (_nextTime == 0 || _nextTime > (uint32)(_vm->_system->getMillis() / 16))
return;
if (togglePlaying(_playing, true)) {
_nextTime = 0;
} else if (_loops == 0 && _timingMode == kLBAutoUserIdle) {
debug(9, "Looping in update()");
setNextTime(_periodMin, _periodMax);
}
}
void LBItem::handleMouseDown(Common::Point pos) {
if (!_loaded || !_enabled || !_globalEnabled)
return;
_vm->setFocus(this);
runScript(kLBEventMouseDown);
runScript(kLBEventMouseTrackIn);
}
void LBItem::handleMouseMove(Common::Point pos) {
// TODO: handle drag
}
void LBItem::handleMouseUp(Common::Point pos) {
_vm->setFocus(NULL);
runScript(kLBEventMouseUp);
runScript(kLBEventMouseUpIn);
}
bool LBItem::togglePlaying(bool playing, bool restart) {
if (playing) {
_vm->queueDelayedEvent(DelayedEvent(this, kLBDelayedEventDone));
return true;
}
if (((_loaded && _enabled && _globalEnabled) || _phase == kLBPhaseNone) && !_playing) {
_playing = togglePlaying(true, restart);
if (_playing) {
_nextTime = 0;
_startTime = _vm->_system->getMillis() / 16;
if (_loopMode == 0xFFFF || _loopMode == 0xFFFE)
_loops = 0xFFFF;
else
_loops = _loopMode;
if (_controlMode >= kLBControlHideMouse) {
debug(2, "Hiding cursor");
_vm->_cursor->hideCursor();
_vm->lockSound(this, true);
if (_controlMode >= kLBControlPauseItems) {
debug(2, "Disabling all");
_vm->setEnableForAll(false, this);
}
}
runScript(kLBEventStarted);
notify(0, _itemId);
}
}
return _playing;
}
void LBItem::done(bool onlyNotify) {
if (onlyNotify) {
if (_relocPoint.x || _relocPoint.y) {
_rect.translate(_relocPoint.x, _relocPoint.y);
// TODO: does drag box need adjusting?
}
if (_loops && --_loops) {
debug(9, "Real looping (now 0x%04x left)", _loops);
setNextTime(_delayMin, _delayMax, _startTime);
} else
done(false);
return;
}
_playing = false;
_loops = 0;
_startTime = 0;
if (_controlMode >= kLBControlHideMouse) {
debug(2, "Showing cursor");
_vm->_cursor->showCursor();
_vm->lockSound(this, false);
if (_controlMode >= kLBControlPauseItems) {
debug(2, "Enabling all");
_vm->setEnableForAll(true, this);
}
}
if (_timingMode == kLBAutoUserIdle) {
debug(9, "Looping in done() - %d to %d", _periodMin, _periodMax);
setNextTime(_periodMin, _periodMax);
}
runScript(kLBEventDone);
notify(0xFFFF, _itemId);
}
void LBItem::init() {
runScript(kLBEventInit);
}
void LBItem::setVisible(bool visible) {
if (visible == _visible)
return;
_visible = visible;
_vm->_needsRedraw = true;
}
void LBItem::setGlobalVisible(bool visible) {
bool wasEnabled = _visible && _globalVisible;
_globalVisible = visible;
if (wasEnabled != (_visible && _globalVisible))
_vm->_needsRedraw = true;
}
void LBItem::startPhase(uint phase) {
if (_phase == phase) {
if (_phase != kLBPhaseNone) {
setEnabled(true);
}
load();
}
switch (phase) {
case kLBPhaseLoad:
runScript(kLBEventListLoad);
break;
case kLBPhaseCreate:
runScript(kLBEventPhaseCreate);
if (_timingMode == kLBAutoCreate) {
debug(2, "Phase create: time startup");
setNextTime(_periodMin, _periodMax);
}
break;
case kLBPhaseInit:
runScript(kLBEventPhaseInit);
if (_timingMode == kLBAutoInit) {
debug(2, "Phase init: time startup");
setNextTime(_periodMin, _periodMax);
}
break;
case kLBPhaseIntro:
runScript(kLBEventPhaseIntro);
if (_timingMode == kLBAutoIntro || _timingMode == kLBAutoUserIdle) {
debug(2, "Phase intro: time startup");
setNextTime(_periodMin, _periodMax);
}
break;
case kLBPhaseMain:
runScript(kLBEventPhaseMain);
if (_timingMode == kLBAutoUserIdle || _timingMode == kLBAutoMain) {
debug(2, "Phase main: time startup");
setNextTime(_periodMin, _periodMax);
}
break;
default:
break;
}
}
void LBItem::stop() {
if (!_playing)
return;
_loops = 0;
seek(0xFFFF);
done(true);
}
void LBItem::notify(uint16 data, uint16 from) {
if (_timingMode == kLBAutoSync) {
// TODO: is this correct?
if (_periodMin == data && _periodMax == from) {
debug(2, "Handling notify 0x%04x (from %d)", data, from);
setNextTime(0, 0);
}
}
runScript(kLBEventNotified, data, from);
}
void LBItem::load() {
if (_loaded)
return;
_loaded = true;
// FIXME: events etc
if (_timingMode == kLBAutoLoad) {
debug(2, "Load: time startup");
setNextTime(_periodMin, _periodMax);
}
}
void LBItem::unload() {
if (!_loaded)
return;
_loaded = false;
// FIXME: stuff
}
void LBItem::moveBy(const Common::Point &pos) {
_rect.translate(pos.x, pos.y);
}
void LBItem::moveTo(const Common::Point &pos) {
_rect.moveTo(pos);
}
LBItem *LBItem::clone(uint16 newId, const Common::String &newName) {
LBItem *item = createClone();
item->_itemId = newId;
item->_desc = newName;
item->_resourceId = _resourceId;
// FIXME: the rest
_page->addClonedItem(item);
// FIXME: zorder?
return item;
}
LBItem *LBItem::createClone() {
return new LBItem(_vm, _page, _rect);
}
void LBItem::runScript(uint event, uint16 data, uint16 from) {
for (uint i = 0; i < _scriptEntries.size(); i++) {
LBScriptEntry *entry = _scriptEntries[i];
if (entry->event != event)
continue;
if (event == kLBEventNotified) {
if ((entry->matchFrom && entry->matchFrom != from) || entry->matchNotify != data)
continue;
}
bool conditionsMatch = true;
for (uint n = 0; n < entry->conditions.size(); n++) {
if (!checkCondition(entry->conditions[n])) {
conditionsMatch = false;
break;
}
}
if (!conditionsMatch)
continue;
if (entry->type == kLBNotifyScript) {
debug(2, "Notify: event 0x%04x, opcode 0x%04x, param 0x%04x",
entry->event, entry->opcode, entry->param);
if (entry->opcode == kLBNotifyGUIAction)
_vm->addNotifyEvent(NotifyEvent(entry->opcode, _itemId));
else if (entry->opcode == kLBNotifyChangeMode && _vm->getGameType() != GType_LIVINGBOOKSV1) {
NotifyEvent notifyEvent(entry->opcode, entry->param);
notifyEvent.newUnknown = entry->newUnknown;
notifyEvent.newMode = entry->newMode;
notifyEvent.newPage = entry->newPage;
notifyEvent.newSubpage = entry->newSubpage;
notifyEvent.newCursor = entry->newCursor;
_vm->addNotifyEvent(notifyEvent);
} else
_vm->addNotifyEvent(NotifyEvent(entry->opcode, entry->param));
} else
runScriptEntry(entry);
}
}
int LBItem::runScriptEntry(LBScriptEntry *entry) {
if (entry->state == 0xffff)
return 0;
uint start = 0;
uint count = entry->argc;
// zero targets = apply to self
if (!count)
count = 1;
if (entry->opcode != kLBOpRunSubentries) switch (entry->param) {
case 0xfffe:
// Run once (disable self after run).
entry->state = 0xffff;
break;
case 0xffff:
break;
case 0:
case 1:
case 2:
start = entry->state;
entry->state++;
if (entry->state >= count) {
switch (entry->param) {
case 0:
// Disable..
entry->state = 0xffff;
return 0;
case 1:
// Stay at the end.
entry->state = count - 1;
break;
case 2:
// Loop.
entry->state = 0;
break;
default:
break;
}
}
count = 1;
break;
case 3:
// Pick random target.
start = _vm->_rnd->getRandomNumberRng(0, count);
count = 1;
break;
default:
warning("Weird param for script entry (type 0x%04x, event 0x%04x, opcode 0x%04x, param 0x%04x)",
entry->type, entry->event, entry->opcode, entry->param);
}
for (uint n = start; n < count; n++) {
LBItem *target;
debug(2, "Script run: type 0x%04x, event 0x%04x, opcode 0x%04x, param 0x%04x",
entry->type, entry->event, entry->opcode, entry->param);
if (entry->argc) {
switch (entry->targetingType) {
case kTargetTypeExpression:
{
// FIXME: this should be EVALUATED
LBValue &tgt = _vm->_variables[entry->targets[n]];
switch (tgt.type) {
case kLBValueItemPtr:
target = tgt.item;
break;
case kLBValueString:
// FIXME: handle 'self', at least
// TODO: correct otherwise? or only self?
target = _vm->getItemByName(tgt.string);
break;
case kLBValueInteger:
target = _vm->getItemById(tgt.integer);
break;
default:
// FIXME: handle list
warning("Target '%s' (by expression) resulted in unknown type, skipping", entry->targets[n].c_str());
continue;
}
}
if (!target) {
debug(2, "Target '%s' (by expression) doesn't exist, skipping", entry->targets[n].c_str());
continue;
}
debug(2, "Target: '%s' (expression '%s')", target->_desc.c_str(), entry->targets[n].c_str());
break;
case kTargetTypeCode:
// FIXME
error("encountered kTargetTypeCode");
break;
case kTargetTypeName:
// FIXME: handle 'self'
target = _vm->getItemByName(entry->targets[n]);
if (!target) {
debug(2, "Target '%s' (by name) doesn't exist, skipping", entry->targets[n].c_str());
continue;
}
debug(2, "Target: '%s' (by name)", target->_desc.c_str());
break;
default:
uint16 targetId = entry->argvTarget[n];
// TODO: is this type, perhaps?
uint16 param = entry->argvParam[n];
target = _vm->getItemById(targetId);
if (!target) {
debug(2, "Target %04x (%04x) doesn't exist, skipping", targetId, param);
continue;
}
debug(2, "Target: %04x (%04x) '%s'", targetId, param, target->_desc.c_str());
}
} else {
target = this;
debug(2, "Self-target on '%s'", _desc.c_str());
}
// an opcode in the form 0x1xx means to run the script for event 0xx
if ((entry->opcode & 0xff00) == 0x0100) {
// FIXME: pass on param
target->runScript(entry->opcode & 0xff);
break;
}
switch (entry->opcode) {
case kLBOpNone:
warning("ignoring kLBOpNone (event 0x%04x, param 0x%04x, target '%s')",
entry->event, entry->param, target->_desc.c_str());
break;
case kLBOpXShow:
// TODO: should be setVisible(true) - not a delayed event -
// when we're doing the param 1/2/3 stuff above?
// and in modern LB this is perhaps just a direct target->setVisible(true)..
if (_vm->getGameType() != GType_LIVINGBOOKSV1)
warning("kLBOpXShow on '%s' is probably broken", target->_desc.c_str());
_vm->queueDelayedEvent(DelayedEvent(this, kLBDelayedEventSetNotVisible));
break;
case kLBOpTogglePlay:
target->togglePlaying(false, true);
break;
case kLBOpSetNotVisible:
target->setVisible(false);
break;
case kLBOpSetVisible:
target->setVisible(true);
break;
case kLBOpDestroy:
target->destroySelf();
break;
case kLBOpRewind:
target->seek(1);
break;
case kLBOpStop:
target->stop();
break;
case kLBOpDisable:
target->setEnabled(false);
break;
case kLBOpEnable:
target->setEnabled(true);
break;
case kLBOpGlobalSetNotVisible:
target->setGlobalVisible(false);
break;
case kLBOpGlobalSetVisible:
target->setGlobalVisible(true);
break;
case kLBOpGlobalDisable:
target->setGlobalEnabled(false);
break;
case kLBOpGlobalEnable:
target->setGlobalEnabled(true);
break;
case kLBOpSeekToEnd:
target->seek(0xFFFF);
break;
case kLBOpMute:
case kLBOpUnmute:
// FIXME
warning("ignoring kLBOpMute/Unmute (event 0x%04x, param 0x%04x, target '%s')",
entry->event, entry->param, target->_desc.c_str());
break;
case kLBOpLoad:
target->load();
break;
case kLBOpPreload:
// FIXME
warning("ignoring kLBOpPreload (event 0x%04x, param 0x%04x, target '%s')",
entry->event, entry->param, target->_desc.c_str());
break;
case kLBOpUnload:
target->unload();
break;
case kLBOpSeekToPrev:
case kLBOpSeekToNext:
// FIXME
warning("ignoring kLBOpSeekToPrev/Next (event 0x%04x, param 0x%04x, target '%s')",
entry->event, entry->param, target->_desc.c_str());
break;
case kLBOpDragBegin:
case kLBOpDragEnd:
// FIXME
warning("ignoring kLBOpDragBegin/End (event 0x%04x, param 0x%04x, target '%s')",
entry->event, entry->param, target->_desc.c_str());
break;
case kLBOpScriptDisable:
case kLBOpScriptEnable:
// FIXME
warning("ignoring kLBOpScriptDisable/Enable (event 0x%04x, param 0x%04x, target '%s')",
entry->event, entry->param, target->_desc.c_str());
break;
case kLBOpUnknown1C:
// FIXME
warning("ignoring kLBOpUnknown1C (event 0x%04x, param 0x%04x, target '%s')",
entry->event, entry->param, target->_desc.c_str());
break;
case kLBOpSendExpression:
_page->_code->runCode(this, entry->offset);
break;
case kLBOpRunSubentries:
for (uint i = 0; i < entry->subentries.size(); i++) {
LBScriptEntry *subentry = entry->subentries[i];
int e = runScriptEntry(subentry);
switch (subentry->opcode) {
case kLBOpJumpUnlessExpression:
debug(2, "JumpUnless got %d (to %d, on %d, of %d)", e, subentry->target, i, entry->subentries.size());
if (!e)
i = subentry->target - 1;
break;
case kLBOpBreakExpression:
debug(2, "BreakExpression");
i = entry->subentries.size();
break;
case kLBOpJumpToExpression:
debug(2, "JumpToExpression got %d (on %d, of %d)", e, i, entry->subentries.size());
i = e - 1;
break;
default:
break;
}
}
break;
case kLBOpRunData:
readData(entry->dataType, entry->dataLen, entry->data);
break;
case kLBOpJumpUnlessExpression:
case kLBOpBreakExpression:
case kLBOpJumpToExpression:
{
LBValue r = _page->_code->runCode(this, entry->offset);
// FIXME
return r.integer;
}
default:
error("Unknown script opcode (type 0x%04x, event 0x%04x, opcode 0x%04x, param 0x%04x, target '%s')",
entry->type, entry->event, entry->opcode, entry->param, target->_desc.c_str());
}
}
return 0;
}
void LBItem::setNextTime(uint16 min, uint16 max) {
setNextTime(min, max, _vm->_system->getMillis() / 16);
}
void LBItem::setNextTime(uint16 min, uint16 max, uint32 start) {
_nextTime = start + _vm->_rnd->getRandomNumberRng((uint)min, (uint)max);
debug(9, "nextTime is now %d frames away", _nextTime - (uint)(_vm->_system->getMillis() / 16));
}
void LBItem::runCommand(const Common::String &command) {
LBCode tempCode(_vm, 0);
debug(2, "running command '%s'", command.c_str());
uint offset = tempCode.parseCode(command);
tempCode.runCode(this, offset);
}
bool LBItem::checkCondition(const Common::String &condition) {
LBCode tempCode(_vm, 0);
debug(3, "checking condition '%s'", condition.c_str());
uint offset = tempCode.parseCode(condition);
LBValue result = tempCode.runCode(this, offset);
return result.toInt();
}
LBSoundItem::LBSoundItem(MohawkEngine_LivingBooks *vm, LBPage *page, Common::Rect rect) : LBItem(vm, page, rect) {
debug(3, "new LBSoundItem");
_running = false;
}
LBSoundItem::~LBSoundItem() {
if (_running)
_vm->_sound->stopSound(_resourceId);
}
void LBSoundItem::update() {
if (_running && !_vm->_sound->isPlaying(_resourceId)) {
_running = false;
done(true);
}
LBItem::update();
}
bool LBSoundItem::togglePlaying(bool playing, bool restart) {
if (!playing)
return LBItem::togglePlaying(playing, restart);
if (_running) {
_running = false;
_vm->_sound->stopSound(_resourceId);
}
if (!_loaded || !_enabled || !_globalEnabled)
return false;
_running = true;
debug(4, "sound %d play for item %d (%s)", _resourceId, _itemId, _desc.c_str());
_vm->playSound(this, _resourceId);
return true;
}
void LBSoundItem::stop() {
if (_running) {
_running = false;
_vm->_sound->stopSound(_resourceId);
}
LBItem::stop();
}
LBItem *LBSoundItem::createClone() {
return new LBSoundItem(_vm, _page, _rect);
}
LBGroupItem::LBGroupItem(MohawkEngine_LivingBooks *vm, LBPage *page, Common::Rect rect) : LBItem(vm, page, rect) {
debug(3, "new LBGroupItem");
_starting = false;
}
void LBGroupItem::readData(uint16 type, uint16 size, Common::MemoryReadStreamEndian *stream) {
switch (type) {
case kLBGroupData:
{
_groupEntries.clear();
uint16 count = stream->readUint16();
debug(3, "Group data: %d entries", count);
if (size != 2 + count * 4)
error("kLBGroupData was wrong size (%d, for %d entries)", size, count);
for (uint i = 0; i < count; i++) {
GroupEntry entry;
// TODO: is type important for any game? at the moment, we ignore it
entry.entryType = stream->readUint16();
entry.entryId = stream->readUint16();
_groupEntries.push_back(entry);
debug(3, "group entry: id %d, type %d", entry.entryId, entry.entryType);
}
}
break;
default:
LBItem::readData(type, size, stream);
}
}
void LBGroupItem::destroySelf() {
LBItem::destroySelf();
for (uint i = 0; i < _groupEntries.size(); i++) {
LBItem *item = _vm->getItemById(_groupEntries[i].entryId);
if (item)
item->destroySelf();
}
}
void LBGroupItem::setEnabled(bool enabled) {
if (_starting) {
_starting = false;
LBItem::setEnabled(enabled);
} else {
for (uint i = 0; i < _groupEntries.size(); i++) {
LBItem *item = _vm->getItemById(_groupEntries[i].entryId);
if (item)
item->setEnabled(enabled);
}
}
}
void LBGroupItem::setGlobalEnabled(bool enabled) {
for (uint i = 0; i < _groupEntries.size(); i++) {
LBItem *item = _vm->getItemById(_groupEntries[i].entryId);
if (item)
item->setGlobalEnabled(enabled);
}
}
bool LBGroupItem::contains(Common::Point point) {
return false;
}
bool LBGroupItem::togglePlaying(bool playing, bool restart) {
for (uint i = 0; i < _groupEntries.size(); i++) {
LBItem *item = _vm->getItemById(_groupEntries[i].entryId);
if (item)
item->togglePlaying(playing, restart);
}
return false;
}
void LBGroupItem::seek(uint16 pos) {
for (uint i = 0; i < _groupEntries.size(); i++) {
LBItem *item = _vm->getItemById(_groupEntries[i].entryId);
if (item)
item->seek(pos);
}
}
void LBGroupItem::setVisible(bool visible) {
for (uint i = 0; i < _groupEntries.size(); i++) {
LBItem *item = _vm->getItemById(_groupEntries[i].entryId);
if (item)
item->setVisible(visible);
}
}
void LBGroupItem::setGlobalVisible(bool visible) {
for (uint i = 0; i < _groupEntries.size(); i++) {
LBItem *item = _vm->getItemById(_groupEntries[i].entryId);
if (item)
item->setGlobalVisible(visible);
}
}
void LBGroupItem::startPhase(uint phase) {
_starting = true;
LBItem::startPhase(phase);
_starting = false;
}
void LBGroupItem::stop() {
for (uint i = 0; i < _groupEntries.size(); i++) {
LBItem *item = _vm->getItemById(_groupEntries[i].entryId);
if (item)
item->stop();
}
}
void LBGroupItem::load() {
for (uint i = 0; i < _groupEntries.size(); i++) {
LBItem *item = _vm->getItemById(_groupEntries[i].entryId);
if (item)
item->load();
}
}
void LBGroupItem::unload() {
for (uint i = 0; i < _groupEntries.size(); i++) {
LBItem *item = _vm->getItemById(_groupEntries[i].entryId);
if (item)
item->unload();
}
}
void LBGroupItem::moveBy(const Common::Point &pos) {
for (uint i = 0; i < _groupEntries.size(); i++) {
LBItem *item = _vm->getItemById(_groupEntries[i].entryId);
if (item)
item->moveBy(pos);
}
}
void LBGroupItem::moveTo(const Common::Point &pos) {
for (uint i = 0; i < _groupEntries.size(); i++) {
LBItem *item = _vm->getItemById(_groupEntries[i].entryId);
if (item)
item->moveTo(pos);
}
}
LBItem *LBGroupItem::createClone() {
// TODO: needed?
error("LBGroupItem::createClone unimplemented");
return new LBGroupItem(_vm, _page, _rect);
}
LBPaletteItem::LBPaletteItem(MohawkEngine_LivingBooks *vm, LBPage *page, Common::Rect rect) : LBItem(vm, page, rect) {
debug(3, "new LBPaletteItem");
_fadeInStart = 0;
_palette = NULL;
}
LBPaletteItem::~LBPaletteItem() {
delete[] _palette;
}
void LBPaletteItem::readData(uint16 type, uint16 size, Common::MemoryReadStreamEndian *stream) {
switch (type) {
case kLBPaletteXData:
{
assert(size >= 8);
_fadeInPeriod = stream->readUint16();
_fadeInStep = stream->readUint16();
_drawStart = stream->readUint16();
_drawCount = stream->readUint16();
if (_drawStart + _drawCount > 256)
error("encountered palette trying to set more than 256 colors");
assert(size == 8 + _drawCount * 4);
// TODO: _drawCount is really more like _drawEnd, so once we're sure that
// there's really no use for the palette entries before _drawCount, we
// might want to just discard them here, at load time.
_palette = new byte[_drawCount * 3];
for (uint i = 0; i < _drawCount; i++) {
_palette[i*3 + 0] = stream->readByte();
_palette[i*3 + 1] = stream->readByte();
_palette[i*3 + 2] = stream->readByte();
stream->readByte();
}
}
break;
default:
LBItem::readData(type, size, stream);
}
}
bool LBPaletteItem::togglePlaying(bool playing, bool restart) {
// TODO: this likely isn't the right place
if (playing) {
_fadeInStart = _vm->_system->getMillis();
_fadeInCurrent = 0;
return true;
}
return LBItem::togglePlaying(playing, restart);
}
void LBPaletteItem::update() {
if (_fadeInStart) {
if (!_palette)
error("LBPaletteItem had no palette on startup");
uint32 elapsedTime = _vm->_system->getMillis() - _fadeInStart;
uint32 divTime = elapsedTime / _fadeInStep;
if (divTime > _fadeInPeriod)
divTime = _fadeInPeriod;
if (_fadeInCurrent != divTime) {
_fadeInCurrent = divTime;
// TODO: actual fading-in
if (_visible && _globalVisible) {
_vm->_system->getPaletteManager()->setPalette(_palette + _drawStart * 3, _drawStart, _drawCount - _drawStart);
_vm->_needsRedraw = true;
}
}
if (elapsedTime >= (uint32)_fadeInPeriod * (uint32)_fadeInStep) {
// TODO: correct?
_fadeInStart = 0;
}
}
LBItem::update();
}
LBItem *LBPaletteItem::createClone() {
error("can't clone LBPaletteItem");
}
LBLiveTextItem::LBLiveTextItem(MohawkEngine_LivingBooks *vm, LBPage *page, Common::Rect rect) : LBItem(vm, page, rect) {
_currentPhrase = 0xFFFF;
_currentWord = 0xFFFF;
debug(3, "new LBLiveTextItem");
}
void LBLiveTextItem::readData(uint16 type, uint16 size, Common::MemoryReadStreamEndian *stream) {
switch (type) {
case kLBLiveTextData:
{
stream->read(_backgroundColor, 4); // unused?
stream->read(_foregroundColor, 4);
stream->read(_highlightColor, 4);
_paletteIndex = stream->readUint16();
uint16 phraseCount = stream->readUint16();
uint16 wordCount = stream->readUint16();
debug(3, "LiveText has %d words in %d phrases, palette index 0x%04x", wordCount, phraseCount, _paletteIndex);
debug(3, "LiveText colors: background %02x%02x%02x%02x, foreground %02x%02x%02x%02x, highlight %02x%02x%02x%02x",
_backgroundColor[0], _backgroundColor[1], _backgroundColor[2], _backgroundColor[3],
_foregroundColor[0], _foregroundColor[1], _foregroundColor[2], _foregroundColor[3],
_highlightColor[0], _highlightColor[1], _highlightColor[2], _highlightColor[3]);
if (size != 18 + 14 * wordCount + 18 * phraseCount)
error("Bad Live Text data size (got %d, wanted %d words and %d phrases)", size, wordCount, phraseCount);
_words.clear();
for (uint i = 0; i < wordCount; i++) {
LiveTextWord word;
word.bounds = _vm->readRect(stream);
word.soundId = stream->readUint16();
word.itemType = stream->readUint16();
word.itemId = stream->readUint16();
debug(4, "Word: (%d, %d) to (%d, %d), sound %d, item %d (type %d)",
word.bounds.left, word.bounds.top, word.bounds.right, word.bounds.bottom, word.soundId, word.itemId, word.itemType);
_words.push_back(word);
}
_phrases.clear();
for (uint i = 0; i < phraseCount; i++) {
LiveTextPhrase phrase;
phrase.wordStart = stream->readUint16();
phrase.wordCount = stream->readUint16();
phrase.highlightStart = stream->readUint16();
phrase.startId = stream->readUint16();
phrase.highlightEnd = stream->readUint16();
phrase.endId = stream->readUint16();
// The original stored the values in uint32's so we need to swap here
if (_vm->isBigEndian()) {
SWAP(phrase.highlightStart, phrase.startId);
SWAP(phrase.highlightEnd, phrase.endId);
}
uint32 unknown1 = stream->readUint16();
uint16 unknown2 = stream->readUint32();
if (unknown1 != 0 || unknown2 != 0)
error("Unexpected unknowns %08x/%04x in LiveText word", unknown1, unknown2);
debug(4, "Phrase: start %d, count %d, start at %d (from %d), end at %d (from %d)",
phrase.wordStart, phrase.wordCount, phrase.highlightStart, phrase.startId, phrase.highlightEnd, phrase.endId);
_phrases.push_back(phrase);
}
}
break;
default:
LBItem::readData(type, size, stream);
}
}
bool LBLiveTextItem::contains(Common::Point point) {
if (!LBItem::contains(point))
return false;
point.x -= _rect.left;
point.y -= _rect.top;
for (uint i = 0; i < _words.size(); i++) {
if (_words[i].bounds.contains(point))
return true;
}
return false;
}
void LBLiveTextItem::paletteUpdate(uint16 word, bool on) {
_vm->_needsRedraw = true;
// Sometimes the last phrase goes out-of-bounds, the original engine
// only checks the words which are valid in the palette updating code.
if (word >= _words.size())
return;
if (_resourceId) {
// with a resource, we draw a bitmap in draw() rather than changing the palette
return;
}
if (on) {
_vm->_system->getPaletteManager()->setPalette(_highlightColor, _paletteIndex + word, 1);
} else {
_vm->_system->getPaletteManager()->setPalette(_foregroundColor, _paletteIndex + word, 1);
}
}
void LBLiveTextItem::update() {
if (_currentWord != 0xFFFF) {
uint16 soundId = _words[_currentWord].soundId;
if (soundId && !_vm->_sound->isPlaying(soundId)) {
paletteUpdate(_currentWord, false);
// TODO: check this in RE
LBItem *item = _vm->getItemById(_words[_currentWord].itemId);
if (item)
item->togglePlaying(false, true);
_currentWord = 0xFFFF;
}
}
LBItem::update();
}
void LBLiveTextItem::draw() {
// this is only necessary when we are drawing using a bitmap
if (!_resourceId)
return;
if (_currentWord != 0xFFFF) {
uint yPos = 0;
if (_currentWord > 0) {
for (uint i = 0; i < _currentWord; i++) {
yPos += (_words[i].bounds.bottom - _words[i].bounds.top);
}
}
drawWord(_currentWord, yPos);
return;
}
if (_currentPhrase == 0xFFFF)
return;
uint wordStart = _phrases[_currentPhrase].wordStart;
uint wordCount = _phrases[_currentPhrase].wordCount;
if (wordStart + wordCount > _words.size())
error("phrase %d was invalid (%d words, from %d, out of only %d total)",
_currentPhrase, wordCount, wordStart, _words.size());
uint yPos = 0;
for (uint i = 0; i < wordStart + wordCount; i++) {
if (i >= wordStart)
drawWord(i, yPos);
yPos += (_words[i].bounds.bottom - _words[i].bounds.top);
}
}
void LBLiveTextItem::drawWord(uint word, uint yPos) {
Common::Rect srcRect(0, yPos, _words[word].bounds.right - _words[word].bounds.left,
yPos + _words[word].bounds.bottom - _words[word].bounds.top);
Common::Rect dstRect = _words[word].bounds;
dstRect.translate(_rect.left, _rect.top);
_vm->_gfx->copyAnimImageSectionToScreen(_resourceId, srcRect, dstRect);
}
void LBLiveTextItem::handleMouseDown(Common::Point pos) {
if (!_loaded || !_enabled || !_globalEnabled || _playing)
return LBItem::handleMouseDown(pos);
pos.x -= _rect.left;
pos.y -= _rect.top;
for (uint i = 0; i < _words.size(); i++) {
if (_words[i].bounds.contains(pos)) {
if (_currentWord != 0xFFFF) {
paletteUpdate(_currentWord, false);
_currentWord = 0xFFFF;
}
uint16 soundId = _words[i].soundId;
if (!soundId) {
// TODO: can we be smarter here, using timing?
warning("ignoring click due to no soundId");
return;
}
_currentWord = i;
_vm->playSound(this, soundId);
paletteUpdate(_currentWord, true);
return;
}
}
return LBItem::handleMouseDown(pos);
}
bool LBLiveTextItem::togglePlaying(bool playing, bool restart) {
if (!playing)
return LBItem::togglePlaying(playing, restart);
if (!_loaded || !_enabled || !_globalEnabled)
return _playing;
// TODO: handle this properly
_vm->_sound->stopSound();
_currentWord = 0xFFFF;
_currentPhrase = 0xFFFF;
return true;
}
void LBLiveTextItem::stop() {
// TODO: stop sound, refresh palette
LBItem::stop();
}
void LBLiveTextItem::notify(uint16 data, uint16 from) {
if (!_loaded || !_enabled || !_globalEnabled || !_playing)
return LBItem::notify(data, from);
if (_currentWord != 0xFFFF) {
// TODO: handle this properly
_vm->_sound->stopSound();
paletteUpdate(_currentWord, false);
_currentWord = 0xFFFF;
}
for (uint i = 0; i < _phrases.size(); i++) {
if (_phrases[i].highlightStart == data && _phrases[i].startId == from) {
debug(2, "Enabling phrase %d", i);
for (uint j = 0; j < _phrases[i].wordCount; j++) {
paletteUpdate(_phrases[i].wordStart + j, true);
}
_currentPhrase = i;
// TODO: not sure this is the correct logic
if (i == _phrases.size() - 1) {
_currentPhrase = 0xFFFF;
done(true);
}
} else if (_phrases[i].highlightEnd == data && _phrases[i].endId == from) {
debug(2, "Disabling phrase %d", i);
for (uint j = 0; j < _phrases[i].wordCount; j++) {
paletteUpdate(_phrases[i].wordStart + j, false);
}
_currentPhrase = 0xFFFF;
}
}
LBItem::notify(data, from);
}
LBItem *LBLiveTextItem::createClone() {
error("can't clone LBLiveTextItem");
}
LBPictureItem::LBPictureItem(MohawkEngine_LivingBooks *vm, LBPage *page, Common::Rect rect) : LBItem(vm, page, rect) {
debug(3, "new LBPictureItem");
}
void LBPictureItem::readData(uint16 type, uint16 size, Common::MemoryReadStreamEndian *stream) {
switch (type) {
case kLBSetDrawMode:
{
assert(size == 2);
// TODO: this probably sets whether points are always contained (0x10)
// or whether the bitmap contents are checked (00, or anything else?)
uint16 val = stream->readUint16();
debug(2, "LBPictureItem: kLBSetDrawMode: %04x", val);
}
break;
default:
LBItem::readData(type, size, stream);
}
}
bool LBPictureItem::contains(Common::Point point) {
if (!LBItem::contains(point))
return false;
if (!_doHitTest)
return true;
// TODO: only check pixels if necessary
return !_vm->_gfx->imageIsTransparentAt(_resourceId, false, point.x - _rect.left, point.y - _rect.top);
}
void LBPictureItem::init() {
_vm->_gfx->preloadImage(_resourceId);
LBItem::init();
}
void LBPictureItem::draw() {
if (!_loaded || !_visible || !_globalVisible)
return;
_vm->_gfx->copyAnimImageToScreen(_resourceId, _rect.left, _rect.top);
}
LBItem *LBPictureItem::createClone() {
return new LBPictureItem(_vm, _page, _rect);
}
LBAnimationItem::LBAnimationItem(MohawkEngine_LivingBooks *vm, LBPage *page, Common::Rect rect) : LBItem(vm, page, rect) {
_anim = NULL;
_running = false;
debug(3, "new LBAnimationItem");
}
LBAnimationItem::~LBAnimationItem() {
delete _anim;
}
void LBAnimationItem::setEnabled(bool enabled) {
if (_running) {
if (enabled && _globalEnabled && !_loaded)
_anim->start();
else if (_loaded && !enabled && _enabled && _globalEnabled)
_anim->stop();
}
return LBItem::setEnabled(enabled);
}
bool LBAnimationItem::contains(Common::Point point) {
if (!LBItem::contains(point))
return false;
if (!_doHitTest)
return true;
return !_anim->transparentAt(point.x, point.y);
}
void LBAnimationItem::update() {
if (_loaded && _enabled && _globalEnabled && _running) {
bool wasDone = _anim->update();
if (wasDone) {
_running = false;
done(true);
}
}
LBItem::update();
}
bool LBAnimationItem::togglePlaying(bool playing, bool restart) {
if (playing) {
if (_loaded && _enabled && _globalEnabled) {
if (restart)
seek(1);
_running = true;
_anim->start();
}
return _running;
}
return LBItem::togglePlaying(playing, restart);
}
void LBAnimationItem::done(bool onlyNotify) {
if (!onlyNotify) {
_anim->stop();
}
LBItem::done(onlyNotify);
}
void LBAnimationItem::init() {
_anim = new LBAnimation(_vm, this, _resourceId);
LBItem::init();
}
void LBAnimationItem::stop() {
if (_running) {
_anim->stop();
seek(0xFFFF);
}
_running = false;
LBItem::stop();
}
void LBAnimationItem::seek(uint16 pos) {
_anim->seek(pos);
}
void LBAnimationItem::seekToTime(uint32 time) {
_anim->seekToTime(time);
}
void LBAnimationItem::startPhase(uint phase) {
if (phase == _phase)
seek(1);
LBItem::startPhase(phase);
}
void LBAnimationItem::draw() {
if (!_visible || !_globalVisible)
return;
_anim->draw();
}
LBItem *LBAnimationItem::createClone() {
LBAnimationItem *item = new LBAnimationItem(_vm, _page, _rect);
item->_anim = new LBAnimation(_vm, item, _resourceId);
return item;
}
LBMovieItem::LBMovieItem(MohawkEngine_LivingBooks *vm, LBPage *page, Common::Rect rect) : LBItem(vm, page, rect) {
debug(3, "new LBMovieItem");
}
LBMovieItem::~LBMovieItem() {
}
void LBMovieItem::update() {
if (_playing) {
VideoEntryPtr video = _vm->_video->findVideo(_resourceId);
if (!video || video->endOfVideo())
done(true);
}
LBItem::update();
}
bool LBMovieItem::togglePlaying(bool playing, bool restart) {
if (playing) {
if ((_loaded && _enabled && _globalEnabled) || _phase == kLBPhaseNone) {
debug("toggled video for phase %d", _phase);
VideoEntryPtr video = _vm->_video->playMovie(_resourceId);
if (!video)
error("Failed to open tMOV %d", _resourceId);
video->moveTo(_rect.left, _rect.top);
return true;
}
}
return LBItem::togglePlaying(playing, restart);
}
LBItem *LBMovieItem::createClone() {
return new LBMovieItem(_vm, _page, _rect);
}
LBMiniGameItem::LBMiniGameItem(MohawkEngine_LivingBooks *vm, LBPage *page, Common::Rect rect) : LBItem(vm, page, rect) {
debug(3, "new LBMiniGameItem");
}
LBMiniGameItem::~LBMiniGameItem() {
}
bool LBMiniGameItem::togglePlaying(bool playing, bool restart) {
// HACK: Since we don't support any of these hardcoded mini games yet,
// just skip to the most logical page. For optional minigames, this
// will return the player to the previous page. For mandatory minigames,
// this will send the player to the next page.
uint16 destPage = 0;
bool returnToMenu = false;
// Figure out what minigame we have and bring us back to a page where
// the player can continue
if (_desc == "Kitch") // Green Eggs and Ham: Kitchen minigame
destPage = 4;
else if (_desc == "Eggs") // Green Eggs and Ham: Eggs minigame
destPage = 5;
else if (_desc == "Fall") // Green Eggs and Ham: Fall minigame
destPage = 13;
else if (_desc == "MagicWrite3") // Arthur's Reading Race: "Let Me Write" minigame (Page 3)
destPage = 3;
else if (_desc == "MagicWrite4") // Arthur's Reading Race: "Let Me Write" minigame (Page 4)
destPage = 4;
else if (_desc == "MagicSpy5") // Arthur's Reading Race: "I Spy" minigame (Page 5)
destPage = 5;
else if (_desc == "MagicSpy6") // Arthur's Reading Race: "I Spy" minigame (Page 6)
destPage = 6;
else if (_desc == "MagicWrite7") // Arthur's Reading Race: "Let Me Write" minigame (Page 7)
destPage = 7;
else if (_desc == "MagicSpy8") // Arthur's Reading Race: "I Spy" minigame (Page 8)
destPage = 8;
else if (_desc == "MagicRace") // Arthur's Reading Race: Race minigame
returnToMenu = true;
else
error("Unknown minigame '%s'", _desc.c_str());
GUI::MessageDialog dialog(Common::String::format("The '%s' minigame is not supported yet.", _desc.c_str()));
dialog.runModal();
// Go back to the menu if requested, otherwise go to the requested page
if (returnToMenu)
_vm->addNotifyEvent(NotifyEvent(kLBNotifyGoToControls, 1));
else
_vm->addNotifyEvent(NotifyEvent(kLBNotifyChangePage, destPage));
return false;
}
LBItem *LBMiniGameItem::createClone() {
error("can't clone LBMiniGameItem");
}
LBProxyItem::LBProxyItem(MohawkEngine_LivingBooks *vm, LBPage *page, Common::Rect rect) : LBItem(vm, page, rect) {
debug(3, "new LBProxyItem");
_page = NULL;
}
LBProxyItem::~LBProxyItem() {
delete _page;
}
void LBProxyItem::load() {
if (_loaded)
return;
Common::String leftover;
Common::String filename = _vm->getFileNameFromConfig("Proxies", _desc.c_str(), leftover);
if (!leftover.empty())
error("LBProxyItem tried loading proxy '%s' but got leftover '%s'", _desc.c_str(), leftover.c_str());
uint16 baseId = 0;
for (uint i = 0; i < filename.size(); i++) {
if (filename[i] == ';') {
baseId = atoi(filename.c_str() + i + 1);
filename = Common::String(filename.c_str(), i);
}
}
debug(1, "LBProxyItem loading archive '%s' with id %d", filename.c_str(), baseId);
Archive *pageArchive = _vm->createArchive();
if (!tryOpenPage(pageArchive, filename))
error("failed to open archive '%s' (for proxy '%s')", filename.c_str(), _desc.c_str());
_page = new LBPage(_vm);
_page->open(pageArchive, baseId);
LBItem::load();
}
void LBProxyItem::unload() {
delete _page;
_page = NULL;
LBItem::unload();
}
LBItem *LBProxyItem::createClone() {
return new LBProxyItem(_vm, _page, _rect);
}
} // End of namespace Mohawk