From 2d924afa9d307c6eaa8126c8e0367b9e1216da5e Mon Sep 17 00:00:00 2001 From: Matthew Hoops Date: Sun, 28 Nov 2010 22:55:15 +0000 Subject: MOHAWK: Add basic Living Books support (all credit goes to fuzzie!) v1 and v3 (both Windows and Mac) are working, v1 support is in better shape. svn-id: r54558 --- engines/mohawk/console.cpp | 1 + engines/mohawk/cursors.cpp | 155 +++ engines/mohawk/cursors.h | 59 +- engines/mohawk/detection_tables.h | 23 +- engines/mohawk/graphics.cpp | 85 +- engines/mohawk/graphics.h | 6 +- engines/mohawk/livingbooks.cpp | 2103 ++++++++++++++++++++++++++++++++++--- engines/mohawk/livingbooks.h | 386 ++++++- engines/mohawk/mohawk.h | 3 +- 9 files changed, 2681 insertions(+), 140 deletions(-) diff --git a/engines/mohawk/console.cpp b/engines/mohawk/console.cpp index e25ff030d0..e4fe9e0f8b 100644 --- a/engines/mohawk/console.cpp +++ b/engines/mohawk/console.cpp @@ -690,6 +690,7 @@ bool LivingBooksConsole::Cmd_DrawImage(int argc, const char **argv) { } _vm->_gfx->copyImageToScreen((uint16)atoi(argv[1])); + _vm->_system->updateScreen(); return false; } diff --git a/engines/mohawk/cursors.cpp b/engines/mohawk/cursors.cpp index 3b937385f9..c7288ab0c3 100644 --- a/engines/mohawk/cursors.cpp +++ b/engines/mohawk/cursors.cpp @@ -30,11 +30,18 @@ #include "mohawk/myst.h" #include "mohawk/riven_cursors.h" +#include "common/macresman.h" +#include "common/ne_exe.h" #include "common/system.h" #include "graphics/cursorman.h" namespace Mohawk { +static const byte s_bwPalette[] = { + 0x00, 0x00, 0x00, 0x00, // Black + 0xFF, 0xFF, 0xFF, 0x00 // White +}; + void CursorManager::showCursor() { CursorMan.showMouse(true); } @@ -43,6 +50,76 @@ void CursorManager::hideCursor() { CursorMan.showMouse(false); } +void CursorManager::setDefaultCursor() { + static const byte defaultCursor[] = { + 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 2, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, + 1, 2, 2, 2, 2, 1, 0, 0, 0, 0, 0, 0, + 1, 2, 2, 2, 2, 2, 1, 0, 0, 0, 0, 0, + 1, 2, 2, 2, 2, 2, 2, 1, 0, 0, 0, 0, + 1, 2, 2, 2, 2, 2, 2, 2, 1, 0, 0, 0, + 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 0, 0, + 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 0, + 1, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, + 1, 2, 2, 2, 1, 2, 2, 1, 0, 0, 0, 0, + 1, 2, 2, 1, 1, 2, 2, 1, 0, 0, 0, 0, + 1, 2, 1, 0, 1, 1, 2, 2, 1, 0, 0, 0, + 1, 1, 0, 0, 0, 1, 2, 2, 1, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 1, 2, 2, 1, 0, 0, + 0, 0, 0, 0, 0, 0, 1, 2, 2, 1, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 1, 0, + 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 1, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0 + }; + + CursorMan.replaceCursor(defaultCursor, 12, 20, 0, 0, 0); + CursorMan.replaceCursorPalette(s_bwPalette, 1, 2); +} + +void CursorManager::setCursor(uint16 id) { + // For the base class, just use the default cursor always + setDefaultCursor(); +} + +void CursorManager::decodeMacXorCursor(Common::SeekableReadStream *stream, byte *cursor) { + assert(stream); + assert(cursor); + + // Get black and white data + for (int i = 0; i < 32; i++) { + byte imageByte = stream->readByte(); + for (int b = 0; b < 8; b++) + cursor[i * 8 + b] = (imageByte & (0x80 >> b)) ? 1 : 2; + } + + // Apply mask data + for (int i = 0; i < 32; i++) { + byte imageByte = stream->readByte(); + for (int b = 0; b < 8; b++) + if ((imageByte & (0x80 >> b)) == 0) + cursor[i * 8 + b] = 0; + } +} + +void DefaultCursorManager::setCursor(uint16 id) { + // The Broderbund devs decided to rip off the Mac format, it seems. + // However, they reversed the x/y hotspot. That makes it totally different!!!! + + Common::SeekableReadStream *stream = _vm->getResource(ID_TCUR, id); + + byte cursorBitmap[16 * 16]; + decodeMacXorCursor(stream, cursorBitmap); + uint16 hotspotY = stream->readUint16BE(); + uint16 hotspotX = stream->readUint16BE(); + + CursorMan.replaceCursor(cursorBitmap, 16, 16, hotspotX, hotspotY, 0); + CursorMan.replaceCursorPalette(s_bwPalette, 1, 2); + + delete stream; +} + MystCursorManager::MystCursorManager(MohawkEngine_Myst *vm) : _vm(vm) { _bmpDecoder = new MystBitmap(); } @@ -83,6 +160,10 @@ void MystCursorManager::setCursor(uint16 id) { delete mhkSurface; } +void MystCursorManager::setDefaultCursor() { + setCursor(kDefaultMystCursor); +} + void RivenCursorManager::setCursor(uint16 id) { // All of Riven's cursors are hardcoded. See riven_cursors.h for these definitions. @@ -192,4 +273,78 @@ void RivenCursorManager::setCursor(uint16 id) { g_system->updateScreen(); } +void RivenCursorManager::setDefaultCursor() { + setCursor(kRivenMainCursor); +} + +NECursorManager::NECursorManager(const Common::String &appName) { + _exe = new Common::NEResources(); + + if (!_exe->loadFromEXE(appName)) { + // Not all have cursors anyway, so this is not a problem + delete _exe; + _exe = 0; + } +} + +NECursorManager::~NECursorManager() { + delete _exe; +} + +void NECursorManager::setCursor(uint16 id) { + if (!_exe) { + Common::Array cursors = _exe->getCursors(); + + for (uint32 i = 0; i < cursors.size(); i++) { + if (cursors[i].id == id) { + Common::NECursor *cursor = cursors[i].cursors[0]; + CursorMan.replaceCursor(cursor->getSurface(), cursor->getWidth(), cursor->getHeight(), cursor->getHotspotX(), cursor->getHotspotY(), 0); + CursorMan.replaceCursorPalette(cursor->getPalette(), 0, 256); + return; + } + } + } + + // Last resort (not all have cursors) + setDefaultCursor(); +} + +MacCursorManager::MacCursorManager(const Common::String &appName) { + _resFork = new Common::MacResManager(); + + if (!_resFork->open(appName)) { + // Not all have cursors anyway, so this is not a problem + delete _resFork; + _resFork = 0; + } +} + +MacCursorManager::~MacCursorManager() { + delete _resFork; +} + +void MacCursorManager::setCursor(uint16 id) { + if (!_resFork) { + setDefaultCursor(); + return; + } + + Common::SeekableReadStream *stream = _resFork->getResource(MKID_BE('CURS'), id); + + if (!stream) { + setDefaultCursor(); + return; + } + + byte cursorBitmap[16 * 16]; + decodeMacXorCursor(stream, cursorBitmap); + uint16 hotspotX = stream->readUint16BE(); + uint16 hotspotY = stream->readUint16BE(); + + CursorMan.replaceCursor(cursorBitmap, 16, 16, hotspotX, hotspotY, 0); + CursorMan.replaceCursorPalette(s_bwPalette, 1, 2); + + delete stream; +} + } // End of namespace Mohawk diff --git a/engines/mohawk/cursors.h b/engines/mohawk/cursors.h index 90858a2421..7ff99a342f 100644 --- a/engines/mohawk/cursors.h +++ b/engines/mohawk/cursors.h @@ -28,6 +28,13 @@ #include "common/scummsys.h" +namespace Common { + class MacResManager; + class NEResources; + class SeekableReadStream; + class String; +} + namespace Mohawk { // 803-805 are animated, one large bmp which is in chunks - these are NEVER USED @@ -57,6 +64,7 @@ enum { kRivenHideCursor = 9000 }; +class MohawkEngine; class MohawkEngine_Myst; class MystBitmap; @@ -67,9 +75,29 @@ public: virtual void showCursor(); virtual void hideCursor(); - virtual void setCursor(uint16 id) = 0; + virtual void setCursor(uint16 id); + virtual void setDefaultCursor(); + +protected: + // Handles the Mac version of the xor/and map cursor + void decodeMacXorCursor(Common::SeekableReadStream *stream, byte *cursor); +}; + +// The default Mohawk cursor manager +// Uses standard tCUR resources +class DefaultCursorManager : public CursorManager { +public: + DefaultCursorManager(MohawkEngine *vm) : _vm(vm) {} + ~DefaultCursorManager() {} + + void setCursor(uint16 id); + +private: + MohawkEngine *_vm; }; +// The cursor manager for Myst +// Uses WDIB + CLRC resources class MystCursorManager : public CursorManager { public: MystCursorManager(MohawkEngine_Myst *vm); @@ -78,18 +106,47 @@ public: void showCursor(); void hideCursor(); void setCursor(uint16 id); + void setDefaultCursor(); private: MohawkEngine_Myst *_vm; MystBitmap *_bmpDecoder; }; + +// The cursor manager for Riven +// Uses hardcoded cursors class RivenCursorManager : public CursorManager { public: RivenCursorManager() {} ~RivenCursorManager() {} void setCursor(uint16 id); + void setDefaultCursor(); +}; + +// The cursor manager for NE exe's +class NECursorManager : public CursorManager { +public: + NECursorManager(const Common::String &appName); + ~NECursorManager(); + + void setCursor(uint16 id); + +private: + Common::NEResources *_exe; +}; + +// The cursor manager for Mac applications +class MacCursorManager : public CursorManager { +public: + MacCursorManager(const Common::String &appName); + ~MacCursorManager(); + + void setCursor(uint16 id); + +private: + Common::MacResManager *_resFork; }; } // End of namespace Mohawk diff --git a/engines/mohawk/detection_tables.h b/engines/mohawk/detection_tables.h index fb086d5d93..48d565055c 100644 --- a/engines/mohawk/detection_tables.h +++ b/engines/mohawk/detection_tables.h @@ -602,6 +602,21 @@ static const MohawkGameDescription gameDescriptions[] = { 0 }, + { + { + "tortoise", + "", + AD_ENTRY1("TORTOISE.512", "dfcf7bff3d0f187832c9897497efde0e"), + Common::EN_ANY, + Common::kPlatformWindows, + ADGF_NO_FLAGS, + Common::GUIO_NONE + }, + GType_LIVINGBOOKSV1, + 0, + "TORTOISE.EXE" + }, + { { "tortoise", @@ -658,7 +673,7 @@ static const MohawkGameDescription gameDescriptions[] = { Common::GUIO_NONE }, GType_LIVINGBOOKSV1, - 0, + GF_NO_READONLY, "ARTHUR.EXE" // FIXME: Check this (ST?) }, @@ -673,7 +688,7 @@ static const MohawkGameDescription gameDescriptions[] = { Common::GUIO_NONE }, GType_LIVINGBOOKSV1, - GF_DEMO, + GF_DEMO | GF_NO_READONLY, "ARTHUR.EXE" }, @@ -733,7 +748,7 @@ static const MohawkGameDescription gameDescriptions[] = { Common::GUIO_NONE }, GType_LIVINGBOOKSV1, - GF_DEMO, + GF_DEMO | GF_NO_READONLY, "GRANDMA.EXE" }, @@ -763,7 +778,7 @@ static const MohawkGameDescription gameDescriptions[] = { Common::GUIO_NONE }, GType_LIVINGBOOKSV1, - GF_DEMO, + GF_DEMO | GF_NO_READONLY, "Just Grandma and Me" }, diff --git a/engines/mohawk/graphics.cpp b/engines/mohawk/graphics.cpp index 65eebf7134..8da0cd07cb 100644 --- a/engines/mohawk/graphics.cpp +++ b/engines/mohawk/graphics.cpp @@ -654,8 +654,10 @@ void RivenGraphics::drawExtrasImage(uint16 id, Common::Rect dstRect) { _dirtyScreen = true; } -LBGraphics::LBGraphics(MohawkEngine_LivingBooks *vm) : GraphicsManager(), _vm(vm) { +LBGraphics::LBGraphics(MohawkEngine_LivingBooks *vm, uint16 width, uint16 height) : GraphicsManager(), _vm(vm) { _bmpDecoder = (_vm->getGameType() == GType_LIVINGBOOKSV1) ? new OldMohawkBitmap() : new MohawkBitmap(); + + initGraphics(width, height, true); } LBGraphics::~LBGraphics() { @@ -669,15 +671,82 @@ MohawkSurface *LBGraphics::decodeImage(uint16 id) { return _bmpDecoder->decodeImage(_vm->getResource(ID_TBMP, id)); } -void LBGraphics::copyImageToScreen(uint16 image, uint16 left, uint16 top) { - Graphics::Surface *surface = findImage(image)->getSurface(); +void LBGraphics::preloadImage(uint16 image) { + findImage(image); +} - uint16 width = MIN(surface->w, _vm->_system->getWidth()); - uint16 height = MIN(surface->h, _vm->_system->getHeight()); - _vm->_system->copyRectToScreen((byte *)surface->pixels, surface->pitch, left, top, width, height); +void LBGraphics::copyImageToScreen(uint16 image, bool useOffsets, int left, int top) { + MohawkSurface *mhkSurface = findImage(image); - // FIXME: Remove this and update only when necessary - _vm->_system->updateScreen(); + if (useOffsets) { + left -= mhkSurface->getOffsetX(); + top -= mhkSurface->getOffsetY(); + } + + uint16 startX = 0; + uint16 startY = 0; + + // TODO: clip rect + if (left < 0) { + startX -= left; + left = 0; + } + + if (top < 0) { + startY -= top; + top = 0; + } + + if (left >= _vm->_system->getWidth()) + return; + if (top >= _vm->_system->getHeight()) + return; + + Graphics::Surface *surface = mhkSurface->getSurface(); + if (startX >= surface->w) + return; + if (startY >= surface->h) + return; + + uint16 width = MIN(surface->w - startX, _vm->_system->getWidth() - left); + uint16 height = MIN(surface->h - startY, _vm->_system->getHeight() - top); + + byte *surf = (byte *)surface->getBasePtr(0, startY); + Graphics::Surface *screen = _vm->_system->lockScreen(); + + // image and screen are always 8bpp for LB + for (uint16 y = 0; y < height; y++) { + byte *dest = (byte *)screen->getBasePtr(left, top + y); + byte *src = surf + startX; + // blit, with 0 being transparent + for (uint16 x = 0; x < width; x++) { + if (*src) + *dest = *src; + src++; + dest++; + } + surf += surface->pitch; + } + + _vm->_system->unlockScreen(); +} + +bool LBGraphics::imageIsTransparentAt(uint16 image, bool useOffsets, int x, int y) { + MohawkSurface *mhkSurface = findImage(image); + + if (useOffsets) { + x += mhkSurface->getOffsetX(); + y += mhkSurface->getOffsetY(); + } + + if (x < 0 || y < 0) + return true; + + Graphics::Surface *surface = mhkSurface->getSurface(); + if (x >= surface->w || y >= surface->h) + return true; + + return *(byte *)surface->getBasePtr(x, y) == 0; } void LBGraphics::setPalette(uint16 id) { diff --git a/engines/mohawk/graphics.h b/engines/mohawk/graphics.h index 38d174b481..71bdf2f4a7 100644 --- a/engines/mohawk/graphics.h +++ b/engines/mohawk/graphics.h @@ -196,11 +196,13 @@ private: class LBGraphics : public GraphicsManager { public: - LBGraphics(MohawkEngine_LivingBooks *vm); + LBGraphics(MohawkEngine_LivingBooks *vm, uint16 width, uint16 height); ~LBGraphics(); - void copyImageToScreen(uint16 image, uint16 left = 0, uint16 top = 0); + void preloadImage(uint16 image); + void copyImageToScreen(uint16 image, bool useOffsets = false, int left = 0, int top = 0); void setPalette(uint16 id); + bool imageIsTransparentAt(uint16 image, bool useOffsets, int x, int y); protected: MohawkSurface *decodeImage(uint16 id); diff --git a/engines/mohawk/livingbooks.cpp b/engines/mohawk/livingbooks.cpp index 0ce8135508..fa3db1d316 100644 --- a/engines/mohawk/livingbooks.cpp +++ b/engines/mohawk/livingbooks.cpp @@ -25,21 +25,66 @@ #include "mohawk/livingbooks.h" #include "mohawk/resource.h" +#include "mohawk/cursors.h" #include "common/events.h" +#include "common/EventRecorder.h" #include "engines/util.h" namespace Mohawk { +// read a null-terminated string from a stream +static Common::String readString(Common::SeekableSubReadStreamEndian *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::SeekableSubReadStreamEndian *stream) { + Common::Rect rect; + + // the V1 mac games have their rects in QuickDraw order + if (getGameType() == GType_LIVINGBOOKSV1 && 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; +} + MohawkEngine_LivingBooks::MohawkEngine_LivingBooks(OSystem *syst, const MohawkGameDescription *gamedesc) : MohawkEngine(syst, gamedesc) { _needsUpdate = false; + _needsRedraw = false; _screenWidth = _screenHeight = 0; + + _curLanguage = 1; + + _alreadyShowedIntro = false; + + _rnd = new Common::RandomSource(); + g_eventRec.registerRandomSource(*_rnd, "livingbooks"); } MohawkEngine_LivingBooks::~MohawkEngine_LivingBooks() { + destroyPage(); + delete _console; delete _gfx; + delete _rnd; _bookInfoFile.clear(); } @@ -47,8 +92,6 @@ Common::Error MohawkEngine_LivingBooks::run() { MohawkEngine::run(); _console = new LivingBooksConsole(this); - _gfx = new LBGraphics(this); - // Load the book info from the detected file loadBookInfo(getBookInfoFileName()); @@ -60,33 +103,43 @@ Common::Error MohawkEngine_LivingBooks::run() { if (!_screenWidth || !_screenHeight) error("Could not find xRes/yRes variables"); - debug("Setting screen size to %dx%d", _screenWidth, _screenHeight); + _gfx = new LBGraphics(this, _screenWidth, _screenHeight); - // TODO: Eventually move this to a LivingBooksGraphics class or similar - initGraphics(_screenWidth, _screenHeight, true); + if (getPlatform() == Common::kPlatformMacintosh) + _cursor = new MacCursorManager(getAppName()); + else + _cursor = new NECursorManager(getAppName()); - loadIntro(); - - debug(1, "Stack Version: %d", getResourceVersion()); - - _gfx->setPalette(1000); - loadSHP(1000); - loadANI(1000); + _cursor->setDefaultCursor(); + _cursor->showCursor(); - // Code to Load Sounds For Debugging... - //for (byte i = 0; i < 30; i++) - // _sound->playSound(1000+i); + if (!loadPage(kLBIntroMode, 1, 0)) + 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 (uint16 i = 0; i < _items.size(); i++) + if (_items[i]->contains(event.mouse)) + found = _items[i]; + + if (found) + found->handleMouseDown(event.mouse); break; + case Common::EVENT_KEYDOWN: switch (event.kbd.keycode) { case Common::KEYCODE_d: @@ -95,18 +148,32 @@ Common::Error MohawkEngine_LivingBooks::run() { _console->onFrame(); } break; + case Common::KEYCODE_SPACE: pauseGame(); break; + + case Common::KEYCODE_ESCAPE: + if (_curMode == kLBIntroMode) + loadPage(kLBControlMode, 1, 0); + break; + + case Common::KEYCODE_RIGHT: + nextPage(); + break; + default: break; } break; + default: break; } } + updatePage(); + if (_needsUpdate) { _system->updateScreen(); _needsUpdate = false; @@ -114,6 +181,12 @@ Common::Error MohawkEngine_LivingBooks::run() { // Cut down on CPU usage _system->delayMillis(10); + + // handle pending notifications + while (_notifyEvents.size()) { + NotifyEvent notifyEvent = _notifyEvents.pop(); + handleNotify(notifyEvent); + } } return Common::kNoError; @@ -132,6 +205,9 @@ void MohawkEngine_LivingBooks::loadBookInfo(const Common::String &filename) { _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 + _poetryMode = (getIntFromConfig("BookInfo", "poetry") == 1); + // The later Living Books games add some more options: // - fNeedPalette (always true?) // - fUse254ColorPalette (always true?) @@ -139,132 +215,314 @@ void MohawkEngine_LivingBooks::loadBookInfo(const Common::String &filename) { // - fDebugWindow (always 0?) } -void MohawkEngine_LivingBooks::loadIntro() { - Common::String filename; +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); + } +} - // We get to try for a few different names! Yay! - filename = getFileNameFromConfig("Intro", "Page1"); +void MohawkEngine_LivingBooks::destroyPage() { + _sound->stopSound(); + _gfx->clearCache(); - // Some store with .r, not sure why. - if (filename.empty()) - filename = getFileNameFromConfig("Intro", "Page1.r"); + _eventQueue.clear(); - if (!filename.empty()) { - MohawkArchive *introArchive = createMohawkArchive(); - if (introArchive->open(filename)) - _mhk.push_back(introArchive); - else - delete introArchive; + for (uint32 i = 0; i < _items.size(); i++) + delete _items[i]; + _items.clear(); + + for (uint32 i = 0; i < _mhk.size(); i++) + delete _mhk[i]; + _mhk.clear(); + + _notifyEvents.clear(); + + _focus = NULL; +} + +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; + + filename = getFileNameFromConfig(name, base); + _readOnly = false; + + if (filename.empty()) { + filename = getFileNameFromConfig(name, base + ".r"); + _readOnly = true; } - filename = getFileNameFromConfig("Intro", "Page2"); + if (getFeatures() & GF_NO_READONLY) { + if (_readOnly) { + // TODO: make this a warning, after some testing? + error("game detection table is bad (remove GF_NO_READONLY)"); + } 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); + } + } - if (filename.empty()) - filename = getFileNameFromConfig("Intro", "Page2.r"); + // TODO: fading between pages + bool fade = false; + if (filename.hasSuffix(" fade")) { + fade = true; + filename = Common::String(filename.c_str(), filename.size() - 5); + } - if (!filename.empty()) { - MohawkArchive *coverArchive = createMohawkArchive(); - if (coverArchive->open(filename)) - _mhk.push_back(coverArchive); - else - delete coverArchive; + MohawkArchive *pageArchive = createMohawkArchive(); + if (!filename.empty() && pageArchive->open(filename)) { + _mhk.push_back(pageArchive); + } else { + delete pageArchive; + debug(2, "Could not find page %d.%d for '%s'", page, subpage, name.c_str()); + return false; } -} -// Only 1 VSRN resource per stack, Id 1000 -uint16 MohawkEngine_LivingBooks::getResourceVersion() { - Common::SeekableReadStream *versionStream = getResource(ID_VRSN, 1000); + debug(1, "Stack Version: %d", getResourceVersion()); - if (versionStream->size() != 2) - warning("Version Record size mismatch"); + _curMode = mode; + _curPage = page; + _curSubPage = subpage; - uint16 version = versionStream->readUint16BE(); + _cursor->showCursor(); - delete versionStream; - return version; + _gfx->setPalette(1000); + loadBITL(1000); + + for (uint32 i = 0; i < _items.size(); i++) + _items[i]->init(); + + _phase = 0; + _introDone = false; + + _needsRedraw = true; + + return true; } -// Multiple SHP# resource per stack.. Optional per Card? -// This record appears to be a list structure of BMAP resource Ids.. -void MohawkEngine_LivingBooks::loadSHP(uint16 resourceId) { - Common::SeekableSubReadStreamEndian *shpStream = wrapStreamEndian(ID_SHP, resourceId); +void MohawkEngine_LivingBooks::updatePage() { + switch (_phase) { + case 0: + for (uint32 i = 0; i < _items.size(); i++) + _items[i]->startPhase(_phase); + + if (_curMode == kLBControlMode) { + // hard-coded control page startup + LBItem *item; + + item = getItemById(10); + if (item) + item->togglePlaying(false); + + switch (_curPage) { + case 1: + debug(2, "updatePage() for control page 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"); + + item = getItemById(12); + if (item) + item->setVisible(false); + item = getItemById(13); + if (item) + item->setVisible(false); + break; + + case 3: + // TODO: hard-coded handling + break; + } + } + _phase++; + break; + + case 1: + for (uint32 i = 0; i < _items.size(); i++) + _items[i]->startPhase(_phase); - if (shpStream->size() < 6) - warning("SHP Record size too short"); + _phase++; + break; - if (shpStream->readUint16() != 3) - warning("SHP Record u0 not 3"); + case 2: + if (!_introDone) + break; - if (shpStream->readUint16() != 0) - warning("SHP Record u1 not 0"); + for (uint32 i = 0; i < _items.size(); i++) + _items[i]->startPhase(_phase); - uint16 idCount = shpStream->readUint16(); - debug(1, "SHP: idCount: %d", idCount); + _phase++; + break; + } - if (shpStream->size() != (idCount * 2) + 6) - warning("SHP Record size mismatch"); + 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 kLBDestroy: + _items.remove_at(i); + delete delayedEvent.item; + if (_focus == delayedEvent.item) + _focus = NULL; + break; + case kLBSetNotVisible: + _items[i]->setVisible(false); + break; + case kLBDone: + _items[i]->done(true); + break; + } - uint16 *idValues = new uint16[idCount]; - for (uint16 i = 0; i < idCount; i++) { - idValues[i] = shpStream->readUint16(); - debug(1, "SHP: BMAP Resource Id %d: %d", i, idValues[i]); + break; + } } - delete[] idValues; - delete shpStream; + for (uint16 i = 0; i < _items.size(); i++) + _items[i]->update(); + + if (_needsRedraw) { + for (uint16 i = 0; i < _items.size(); i++) + _items[i]->draw(); + + _needsUpdate = true; + } } -// Multiple ANI resource per stack.. Optional per Card? -void MohawkEngine_LivingBooks::loadANI(uint16 resourceId) { - Common::SeekableSubReadStreamEndian *aniStream = wrapStreamEndian(ID_ANI, resourceId); +LBItem *MohawkEngine_LivingBooks::getItemById(uint16 id) { + for (uint16 i = 0; i < _items.size(); i++) + if (_items[i]->getId() == id) + return _items[i]; - if (aniStream->size() != 30) - warning("ANI Record size mismatch"); + return NULL; +} - if (aniStream->readUint16() != 1) - warning("ANI Record u0 not 0"); // Version? - - uint16 u1 = aniStream->readUint16(); - debug(1, "ANI u1: %d", u1); - - uint16 u2 = aniStream->readUint16(); - debug(1, "ANI u2: %d", u2); - - Common::Rect u3; - u3.right = aniStream->readUint16(); - u3.bottom = aniStream->readUint16(); - u3.left = aniStream->readUint16(); - u3.top = aniStream->readUint16(); - debug(1, "ANI u3: (%d, %d), (%d, %d)", u3.left, u3.top, u3.right, u3.bottom); - - Common::Rect u4; - u4.right = aniStream->readUint16(); - u4.bottom = aniStream->readUint16(); - u4.left = aniStream->readUint16(); - u4.top = aniStream->readUint16(); - debug(1, "ANI u4: (%d, %d), (%d, %d)", u4.left, u4.top, u4.right, u4.bottom); - - // BMAP Id? - uint16 u4ResourceId = aniStream->readUint16(); - debug(1, "ANI u4ResourceId: %d", u4ResourceId); - - // Following 3 unknowns also resourceIds in Unused? - uint16 u5 = aniStream->readUint16(); - debug(1, "ANI u5: %d", u5); - if (u5 != 0) - warning("ANI u5 non-zero"); - - uint16 u6 = aniStream->readUint16(); - debug(1, "ANI u6: %d", u6); - if (u6 != 0) - warning("ANI u6 non-zero"); - - uint16 u7 = aniStream->readUint16(); - debug(1, "ANI u7: %d", u7); - if (u7 != 0) - warning("ANI u7 non-zero"); +void MohawkEngine_LivingBooks::setFocus(LBItem *focus) { + _focus = focus; +} - delete aniStream; +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); +} + +// Only 1 VSRN resource per stack, Id 1000 +uint16 MohawkEngine_LivingBooks::getResourceVersion() { + Common::SeekableReadStream *versionStream = getResource(ID_VRSN, 1000); + + if (versionStream->size() != 2) + warning("Version Record size mismatch"); + + uint16 version = versionStream->readUint16BE(); + + delete versionStream; + return version; +} + +void MohawkEngine_LivingBooks::loadBITL(uint16 resourceId) { + Common::SeekableSubReadStreamEndian *bitlStream = wrapStreamEndian(ID_BITL, resourceId); + + while (true) { + Common::Rect rect = readRect(bitlStream); + uint16 type = bitlStream->readUint16(); + + LBItem *res; + switch (type) { + case kLBPictureItem: + res = new LBPictureItem(this, rect); + break; + case kLBAnimationItem: + res = new LBAnimationItem(this, rect); + break; + case kLBPaletteItem: + res = new LBPaletteItem(this, rect); + break; + case kLBGroupItem: + res = new LBGroupItem(this, rect); + break; + case kLBSoundItem: + res = new LBSoundItem(this, rect); + break; + case kLBLiveTextItem: + res = new LBLiveTextItem(this, rect); + break; + default: + warning("Unknown item type %04x", type); + res = new LBItem(this, rect); + break; + } + + res->readFrom(bitlStream); + _items.push_back(res); + + if (bitlStream->size() == bitlStream->pos()) + break; + } } Common::SeekableSubReadStreamEndian *MohawkEngine_LivingBooks::wrapStreamEndian(uint32 tag, uint16 id) { @@ -288,9 +546,8 @@ Common::String MohawkEngine_LivingBooks::getFileNameFromConfig(const Common::Str } Common::String MohawkEngine_LivingBooks::removeQuotesFromString(const Common::String &string) { - // The last char isn't necessarily a quote, the line could have "fade" in it, - // most likely representing to fade to that page. Hopefully it really is that - // obvious :P + // The last char isn't necessarily a quote, the line could have "fade" in it + // (which is then handled in loadPage). // Some versions wrap in quotations, some don't... Common::String tmp = string; @@ -307,7 +564,9 @@ Common::String MohawkEngine_LivingBooks::removeQuotesFromString(const Common::St Common::String MohawkEngine_LivingBooks::convertMacFileName(const Common::String &string) { Common::String filename; - for (uint32 i = 1; i < string.size(); i++) { // First character should be ignored (another colon) + for (uint32 i = 0; i < string.size(); i++) { + if (i == 0 && string[i] == ':') // First character should be ignored (another colon) + continue; if (string[i] == ':') filename += '/'; else @@ -334,4 +593,1612 @@ MohawkArchive *MohawkEngine_LivingBooks::createMohawkArchive() const { return (getGameType() == GType_LIVINGBOOKSV1) ? new LivingBooksArchive_v1() : new MohawkArchive(); } +void MohawkEngine_LivingBooks::addNotifyEvent(NotifyEvent event) { + _notifyEvents.push(event); +} + +void MohawkEngine_LivingBooks::nextPage() { + // we try the next subpage first + if (loadPage(_curMode, _curPage, _curSubPage + 1)) + return; + + // then the first subpage of the next page + if (loadPage(_curMode, _curPage + 1, 1)) + return; + + // then just the next page + if (loadPage(_curMode, _curPage + 1, 0)) + return; + + // then we go back to the control page.. + if (loadPage(kLBControlMode, 1, 0)) + return; + + error("Could not find page after %d.%d for mode %d", _curPage, _curSubPage, (int)_curMode); +} + +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. + + LBItem *item; + switch (_curPage) { + case 1: + // main menu + // TODO: poetry mode + + switch (event.param) { + case 1: + // TODO: page 2 in some versions? + loadPage(kLBControlMode, 3, 0); + 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(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(true); + } + break; + + case 4: + // TODO: page 3 in some versions? + loadPage(kLBControlMode, 2, 0); + break; + + case 10: + item = getItemById(10); + if (item) + item->destroySelf(); + item = getItemById(11); + if (item) + item->setVisible(true); + if (item) + item->togglePlaying(false); + break; + + case 11: + item = getItemById(11); + if (item) + item->togglePlaying(true); + break; + + case 12: + // start game, in play mode + loadPage(kLBPlayMode, 1, 0); + break; + + default: + if (event.param >= 100 && event.param < 100 + (uint)_numLanguages) { + // TODO: language selection? + warning("no language selection yet"); + } else if (event.param >= 200 && event.param < 200 + (uint)_numLanguages) { + // start game, in read mode + loadPage(kLBReadMode, 1, 0); + } + break; + } + break; + + case 2: + // quit screen + + switch (event.param) { + case 1: + case 2: + // button clicked, run animation + item = getItemById(10); + if (item) + item->destroySelf(); + item = getItemById(11); + if (item) + item->destroySelf(); + item = getItemById((event.param == 1) ? 12 : 13); + if (item) { + item->setVisible(true); + item->togglePlaying(false); + } + break; + + case 10: + case 11: + item = getItemById(11); + if (item) + item->togglePlaying(true); + break; + + case 12: + // 'yes', I want to quit + quitGame(); + break; + + case 13: + // 'no', go back to menu + loadPage(kLBControlMode, 1, 0); + break; + } + break; + + case 3: + // options screen + + switch (event.param) { + case 1: + item = getItemById(10); + if (item) + item->destroySelf(); + item = getItemById(202); + if (item) { + item->setVisible(true); + item->togglePlaying(true); + } + break; + + case 2: + item = getItemById(2); + if (item) + item->seek(1); + // TODO: book seeking + break; + + case 3: + item = getItemById(3); + if (item) + item->seek(1); + // TODO: book seeking + break; + + case 4: + loadPage(kLBCreditsMode, 1, 0); + break; + + case 5: + loadPage(kLBPreviewMode, 1, 0); + break; + + case 202: + // TODO: loadPage(kLBPlayMode, book); + break; + } + break; + } + break; + + case kLBNotifyGoToControls: + debug(2, "kLBNotifyGoToControls: %d", event.param); + + if (!loadPage(kLBControlMode, 1, 0)) + error("couldn't load controls page"); + break; + + case kLBNotifyChangePage: + { + debug("kLBNotifyChangePage: %d", event.param); + + switch (event.param) { + case 0xfffe: + nextPage(); + return; + + case 0xffff: + warning("ChangePage unimplemented"); + // TODO: move backwards one page + break; + + default: + warning("ChangePage unimplemented"); + // TODO: set page as specified + break; + } + + // TODO: on bad page: + // if mode < 3 (intro/controls) or mode > 4, move to 2/2 (controls/options?) + // else, move to 2/1 (kLBControlsMode, page 1) + } + 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 kLBNotifyQuit: + debug(2, "kLBNotifyQuit: %d", event.param); + + quitGame(); + break; + + case kLBNotifyCursorChange: + debug(2, "kLBNotifyCursorChange: %d", event.param); + + // TODO: show/hide cursor according to parameter? + 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) { + _currentCel = 0; + + 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->getGameType() != GType_LIVINGBOOKSV1) { + Common::Point offset = _parent->getOffset(_currentCel - 1); + xOffset -= offset.x; + yOffset -= offset.y; + } + + _vm->_gfx->copyImageToScreen(resourceId, true, xOffset, yOffset); +} + +void LBAnimationNode::reset() { + // TODO: this causes stupid flickering + //if (_currentCel) + // _vm->_needsRedraw = true; + + _currentCel = 0; + _currentEntry = 0; + + _xPos = 0; + _yPos = 0; +} + +NodeState LBAnimationNode::update(bool seeking) { + if (_currentEntry == _scriptEntries.size()) + return kLBNodeDone; + + 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; + } + + assert(entry.size == 4); + uint16 strLen = READ_BE_UINT16(entry.data + 2); + + if (strLen) + error("String length for unnamed wave file"); + + switch (entry.opcode) { + case kLBAnimOpPlaySound: + if (seeking) + break; + debug(4, "a: PlaySound(%0d)", soundResourceId); + _vm->_sound->playSound(soundResourceId); + break; + case kLBAnimOpWaitForSound: + if (seeking) + break; + debug(4, "b: WaitForSound(%0d)", soundResourceId); + if (!_vm->_sound->isPlaying(soundResourceId)) + 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; + } + } + break; + + case kLBAnimOpSetTempo: + case kLBAnimOpUnknownE: // TODO: complete guesswork, not in 1.x + { + assert(entry.size == 2); + uint16 tempo = (int16)READ_BE_UINT16(entry.data); + + debug(4, "3: SetTempo(%d)", tempo); + if (entry.opcode == kLBAnimOpUnknownE) { + debug(4, "(beware, stupid OpUnknownE guesswork)"); + } + + _parent->setTempo(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; + + 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->getGameType() != GType_LIVINGBOOKSV1) { + 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 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])); + + _running = false; + _done = false; + _tempo = 1; +} + +LBAnimation::~LBAnimation() { + for (uint32 i = 0; i < _nodes.size(); i++) + delete _nodes[i]; +} + +void LBAnimation::loadShape(uint16 resourceId) { + if (resourceId == 0) + return; + + Common::SeekableSubReadStreamEndian *shapeStream = _vm->wrapStreamEndian(ID_SHP, resourceId); + + if (_vm->getGameType() == GType_LIVINGBOOKSV1) { + 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); +} + +void LBAnimation::update() { + if (!_running) + return; + + if (_vm->_system->getMillis() / 16 <= _lastTime + (uint32)_tempo) + return; + + // the second check is to try 'catching up' with lagged animations, might be crazy + if (_lastTime == 0 || (_vm->_system->getMillis() / 16) > _lastTime + (uint32)(_tempo * 2)) + _lastTime = _vm->_system->getMillis() / 16; + else + _lastTime += _tempo; + + 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) { + _running = false; + _done = true; + } +} + +void LBAnimation::start() { + _lastTime = 0; + _currentFrame = 0; + _running = true; + _done = false; + + for (uint32 i = 0; i < _nodes.size(); i++) + _nodes[i]->reset(); +} + +void LBAnimation::seek(uint16 pos) { + start(); + + 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::stop() { + _running = false; + _done = false; +} + +bool LBAnimation::wasDone() { + if (!_done) + return false; + + _done = 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() { + argvParam = NULL; + argvTarget = NULL; +} + +LBScriptEntry::~LBScriptEntry() { + delete[] argvParam; + delete[] argvTarget; +} + +LBItem::LBItem(MohawkEngine_LivingBooks *vm, Common::Rect rect) : _vm(vm), _rect(rect) { + _phase = 0; + _timingMode = 0; + _delayMin = 0; + _delayMax = 0; + _loopMode = 0; + _loopCount = 0; + _periodMin = 0; + _periodMax = 0; + _controlMode = 0; + + _neverEnabled = true; + _enabled = false; + _visible = true; + _playing = false; + _nextTime = 0; + _startTime = 0; + _loops = 0; +} + +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 = 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) { + uint16 dataType = stream->readUint16(); + uint16 dataSize = stream->readUint16(); + + debug(4, "Data type %04x, size %d", dataType, dataSize); + readData(dataType, dataSize, stream); + + if (stream->pos() == endPos) + break; + + if (stream->pos() > endPos) + error("Read off the end (at %d) of data (ends at %d)", stream->pos(), endPos); + + assert(!stream->eos()); + } +} + +void LBItem::readData(uint16 type, uint16 size, Common::SeekableSubReadStreamEndian *stream) { + switch (type) { + case kLBMsgListScript: + case kLBNotifyScript: + { + if (size < 6) + error("Script entry of type 0x%04x was too small (%d)", type, size); + + LBScriptEntry *entry = new LBScriptEntry; + entry->type = type; + entry->action = stream->readUint16(); + entry->opcode = stream->readUint16(); + entry->param = stream->readUint16(); + debug(4, "Script entry: type 0x%04x, action 0x%04x, opcode 0x%04x, param 0x%04x", + entry->type, entry->action, entry->opcode, entry->param); + + if (type == kLBMsgListScript) { + if (size < 8) + error("Script entry of type 0x%04x was too small (%d)", type, size); + + entry->argc = stream->readUint16(); + entry->argvParam = new uint16[entry->argc]; + entry->argvTarget = new uint16[entry->argc]; + debug(4, "With %d targets:", entry->argc); + + if (size < (8 + 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]); + } + + if (size > (8 + entry->argc * 4)) { + // TODO + warning("Skipping %d probably-important bytes", size - (8 + entry->argc * 4)); + stream->skip(size - (8 + entry->argc * 4)); + } + } else { + if (size > 6) { + // TODO + warning("Skipping %d probably-important bytes", size - 6); + stream->skip(size - 6); + } + } + + _scriptEntries.push_back(entry); + } + 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(); + _periodMin = stream->readUint16(); + _periodMax = stream->readUint16(); + _relocPoint.x = stream->readSint16(); + _relocPoint.y = stream->readSint16(); + _controlMode = stream->readUint16(); + uint16 unknown10 = stream->readUint16(); + // TODO: unknowns + + debug(2, "kLBSetPlayInfo: loop mode %d (%d to %d), timing mode %d (%d to %d), reloc (%d, %d), unknowns %04x, %04x", + _loopMode, _delayMin, _delayMax, + _timingMode, _periodMin, _periodMax, + _relocPoint.x, _relocPoint.y, + _controlMode, unknown10); + } + break; + + case kLBSetPlayPhase: + if (size != 2) + error("SetPlayPhase had wrong size (%d)", size); + _phase = stream->readUint16(); + break; + + case 0x70: + debug(2, "LBItem: 0x70"); + // TODO + break; + + case 0x7b: + assert(size == 0); + debug(2, "LBItem: 0x7b"); + // TODO + break; + + case 0x69: + // TODO: ?? + case 0x6a: + // TODO: ?? + case 0x6d: + // TODO: one-shot? + default: + for (uint i = 0; i < size; i++) + debugN("%02x ", stream->readByte()); + warning("Unknown message %04x (size 0x%04x)", type, size); + break; + } +} + +void LBItem::destroySelf() { + if (!this->_itemId) + error("destroySelf() on an item which was already dead"); + + _vm->queueDelayedEvent(DelayedEvent(this, kLBDestroy)); + + _itemId = 0; +} + +void LBItem::setEnabled(bool enabled) { + if (enabled && _neverEnabled && !_playing) { + if (_timingMode == 2) { + setNextTime(_periodMin, _periodMax); + debug(2, "Enable time startup"); + } + } + + _neverEnabled = false; + _enabled = enabled; +} + +bool LBItem::contains(Common::Point point) { + if (_playing && _loopMode == 0xFFFF) + stop(); + + if (!_playing && _timingMode == 2) + setNextTime(_periodMin, _periodMax); + + return _visible && _rect.contains(point); +} + +void LBItem::update() { + if (_neverEnabled || !_enabled) + return; + + if (_nextTime == 0 || _nextTime > (uint32)(_vm->_system->getMillis() / 16)) + return; + + if (togglePlaying(_playing)) { + _nextTime = 0; + } else if (_loops == 0 && _timingMode == 2) { + debug(9, "Looping in update()"); + setNextTime(_periodMin, _periodMax); + } +} + +void LBItem::handleMouseDown(Common::Point pos) { + if (_neverEnabled || !_enabled) + return; + + _vm->setFocus(this); + runScript(kLBActionMouseDown); +} + +void LBItem::handleMouseMove(Common::Point pos) { + // TODO: handle drag +} + +void LBItem::handleMouseUp(Common::Point pos) { + _vm->setFocus(NULL); + runScript(kLBActionMouseUp); +} + +bool LBItem::togglePlaying(bool playing) { + if (playing) { + _vm->queueDelayedEvent(DelayedEvent(this, kLBDone)); + return true; + } + if (!_neverEnabled && _enabled && !_playing) { + _playing = togglePlaying(true); + if (_playing) { + seek(1); // TODO: this is not good in many situations + _nextTime = 0; + _startTime = _vm->_system->getMillis() / 16; + + if (_loopMode == 0xFFFF || _loopMode == 0xFFFE) + _loops = 0xFFFF; + else + _loops = _loopMode; + + if (_controlMode >= 1) { + debug(2, "Hiding cursor"); + _vm->_cursor->hideCursor(); + // TODO: lock sound? + + if (_controlMode >= 2) { + debug(2, "Disabling all"); + _vm->setEnableForAll(false, this); + } + } + + runScript(kLBActionStarted); + 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 >= 1) { + debug(2, "Showing cursor"); + _vm->_cursor->showCursor(); + // TODO: unlock sound? + + if (_controlMode >= 2) { + debug(2, "Enabling all"); + _vm->setEnableForAll(true, this); + } + } + + if (_timingMode == 2) { + debug(9, "Looping in done() - %d to %d", _periodMin, _periodMax); + setNextTime(_periodMin, _periodMax); + } + + runScript(kLBActionDone); + notify(0xFFFF, _itemId); +} + +void LBItem::setVisible(bool visible) { + if (visible == _visible) + return; + + _visible = visible; + _vm->_needsRedraw = true; +} + +void LBItem::startPhase(uint phase) { + if (_phase == phase) + setEnabled(true); + + switch (phase) { + case 0: + runScript(kLBActionPhase0); + break; + case 1: + runScript(kLBActionPhase1); + if (_timingMode == 1 || _timingMode == 2) { + debug(2, "Phase 1 time startup"); + setNextTime(_periodMin, _periodMax); + } + break; + case 2: + runScript(kLBActionPhase2); + if (_timingMode == 2 || _timingMode == 3) { + debug(2, "Phase 2 time startup"); + setNextTime(_periodMin, _periodMax); + } + break; + } +} + +void LBItem::stop() { + if (!_playing) + return; + + _loops = 0; + seek(0xFFFF); + done(true); +} + +void LBItem::notify(uint16 data, uint16 from) { + if (_timingMode != 4) + return; + + // TODO: is this correct? + if (_periodMin != from) + return; + if (_periodMax != data) + return; + + debug(2, "Handling notify 0x%04x (from %d)", data, from); + setNextTime(0, 0); +} + +void LBItem::runScript(uint id) { + for (uint i = 0; i < _scriptEntries.size(); i++) { + LBScriptEntry *entry = _scriptEntries[i]; + if (entry->action != id) + continue; + + if (entry->type == kLBNotifyScript) { + if (entry->opcode == kLBNotifyGUIAction) + _vm->addNotifyEvent(NotifyEvent(entry->opcode, _itemId)); + else + _vm->addNotifyEvent(NotifyEvent(entry->opcode, entry->param)); + } else { + if (entry->param != 0xffff) { + // TODO: if param is 1/2/3.. + warning("Ignoring script entry (type 0x%04x, action 0x%04x, opcode 0x%04x, param 0x%04x)", + entry->type, entry->action, entry->opcode, entry->param); + continue; + } + + for (uint n = 0; n < entry->argc; n++) { + uint16 targetId = entry->argvTarget[n]; + // TODO: is this type, perhaps? + uint16 param = entry->argvParam[n]; + LBItem *target = _vm->getItemById(targetId); + + debug(2, "Script run: type 0x%04x, action 0x%04x, opcode 0x%04x, param 0x%04x, target id %d", + entry->type, entry->action, entry->opcode, entry->param, targetId); + + if (!target) + continue; + + switch (entry->opcode) { + case 1: + // TODO: should be setVisible(true) - not a delayed event - + // when we're doing the param 1/2/3 stuff above? + _vm->queueDelayedEvent(DelayedEvent(this, kLBSetNotVisible)); + break; + + case 2: + target->togglePlaying(false); + break; + + case 3: + target->setVisible(false); + break; + + case 4: + target->setVisible(true); + break; + + case 5: + target->destroySelf(); + break; + + case 6: + target->seek(1); + break; + + case 7: + target->stop(); + break; + + case 8: + target->setEnabled(false); + break; + + case 9: + target->setEnabled(true); + break; + + case 0xf: // apply palette? seen in greeneggs + default: + // TODO + warning("Ignoring script entry (type 0x%04x, action 0x%04x, opcode 0x%04x, param 0x%04x) for %d (param %04x)", + entry->type, entry->action, entry->opcode, entry->param, targetId, param); + } + } + } + } +} + +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)); +} + +LBSoundItem::LBSoundItem(MohawkEngine_LivingBooks *vm, Common::Rect rect) : LBItem(vm, rect) { + debug(3, "new LBSoundItem"); +} + +LBSoundItem::~LBSoundItem() { + _vm->_sound->stopSound(_resourceId); +} + +bool LBSoundItem::togglePlaying(bool playing) { + if (!playing) + return LBItem::togglePlaying(playing); + + _vm->_sound->stopSound(_resourceId); + + if (_neverEnabled || !_enabled) + return false; + + _vm->_sound->playSound(_resourceId, Audio::Mixer::kMaxChannelVolume, false); + return true; +} + +void LBSoundItem::stop() { + _vm->_sound->stopSound(_resourceId); + + LBItem::stop(); +} + +LBGroupItem::LBGroupItem(MohawkEngine_LivingBooks *vm, Common::Rect rect) : LBItem(vm, rect) { + debug(3, "new LBGroupItem"); + _starting = false; +} + +void LBGroupItem::readData(uint16 type, uint16 size, Common::SeekableSubReadStreamEndian *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::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); + } + } +} + +bool LBGroupItem::contains(Common::Point point) { + return false; +} + +bool LBGroupItem::togglePlaying(bool playing) { + for (uint i = 0; i < _groupEntries.size(); i++) { + LBItem *item = _vm->getItemById(_groupEntries[i].entryId); + if (item) + item->togglePlaying(playing); + } + + 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::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(); + } +} + +LBPaletteItem::LBPaletteItem(MohawkEngine_LivingBooks *vm, Common::Rect rect) : LBItem(vm, rect) { + debug(3, "new LBPaletteItem"); +} + +void LBPaletteItem::readData(uint16 type, uint16 size, Common::SeekableSubReadStreamEndian *stream) { + switch (type) { + case 0x72: + { + assert(size == 4 + 256 * 4); + // TODO + _start = stream->readUint16(); + _count = stream->readUint16(); + stream->read(_palette, 256 * 4); + } + break; + + case 0x75: + assert(size == 0); + debug(2, "LBPaletteItem: 0x75"); + // TODO + break; + + default: + LBItem::readData(type, size, stream); + } +} + +void LBPaletteItem::startPhase(uint phase) { + //if (_phase != phase) + // return; + + /*printf("palette: start %d, count %d\n", _start, _count); + byte *localpal = _palette; + for (unsigned int i = 0; i < 256 * 4; i++) { + printf("%02x ", *localpal++); + } + printf("\n");*/ + + // TODO: huh? + if (_start != 1) + return; + + // TODO + //_vm->_system->setPalette(_start - 1, _count - (_start - 1), _palette + (_start * 4)); + _vm->_system->setPalette(_palette + _start * 4, 0, 256 - _start); +} + +LBLiveTextItem::LBLiveTextItem(MohawkEngine_LivingBooks *vm, Common::Rect rect) : LBItem(vm, rect) { + _running = false; + debug(3, "new LBLiveTextItem"); +} + +void LBLiveTextItem::readData(uint16 type, uint16 size, Common::SeekableSubReadStreamEndian *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(); + // TODO: unknowns + uint16 unknown1 = stream->readUint16(); + uint16 unknown2 = stream->readUint16(); + debug(4, "Word: (%d, %d) to (%d, %d), sound %d, unknowns %04x, %04x", + word.bounds.left, word.bounds.top, word.bounds.right, word.bounds.bottom, word.soundId, unknown1, unknown2); + _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); + } +} + +void LBLiveTextItem::notify(uint16 data, uint16 from) { + if (!_paletteIndex) { + // TODO + warning("Zero palette-index for LiveText; V3 game?"); + return; + } + + for (uint i = 0; i < _phrases.size(); i++) { + // TODO + if (_phrases[i].highlightStart == data && _phrases[i].startId == from) { + debug(2, "Enabling phrase %d", i); + for (uint j = 0; j < _phrases[i].wordCount; j++) { + uint n = _phrases[i].wordStart + j; + _vm->_system->setPalette(_highlightColor, _paletteIndex + n, 1); + } + } 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++) { + uint n = _phrases[i].wordStart + j; + _vm->_system->setPalette(_foregroundColor, _paletteIndex + n, 1); + } + } + } + + LBItem::notify(data, from); +} + +LBPictureItem::LBPictureItem(MohawkEngine_LivingBooks *vm, Common::Rect rect) : LBItem(vm, rect) { + debug(3, "new LBPictureItem"); +} + +void LBPictureItem::readData(uint16 type, uint16 size, Common::SeekableSubReadStreamEndian *stream) { + switch (type) { + case 0x6b: + { + 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: 0x6b: %04x", val); + } + break; + + default: + LBItem::readData(type, size, stream); + } +} + +bool LBPictureItem::contains(Common::Point point) { + if (!LBItem::contains(point)) + return false; + + // 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); +} + +void LBPictureItem::draw() { + if (!_visible) + return; + + _vm->_gfx->copyImageToScreen(_resourceId, false, _rect.left, _rect.top); +} + +LBAnimationItem::LBAnimationItem(MohawkEngine_LivingBooks *vm, Common::Rect rect) : LBItem(vm, rect) { + _anim = NULL; + _running = false; + debug(3, "new LBAnimationItem"); +} + +LBAnimationItem::~LBAnimationItem() { + // TODO: handle this properly + if (_running) + _vm->_sound->stopSound(); + + delete _anim; +} + +void LBAnimationItem::setEnabled(bool enabled) { + if (_running) { + if (enabled && _neverEnabled) + _anim->start(); + else if (!_neverEnabled && !enabled && _enabled) + if (_running) { + _anim->stop(); + + // TODO: handle this properly + _vm->_sound->stopSound(); + } + } + + return LBItem::setEnabled(enabled); +} + +bool LBAnimationItem::contains(Common::Point point) { + return LBItem::contains(point) && !_anim->transparentAt(point.x, point.y); +} + +void LBAnimationItem::update() { + if (!_neverEnabled && _enabled && _running) { + _anim->update(); + } + + LBItem::update(); + + // TODO: where exactly does this go? + // TODO: what checks should we have around this? + if (!_neverEnabled && _enabled && _running && _anim->wasDone()) { + done(true); + } +} + +bool LBAnimationItem::togglePlaying(bool playing) { + if (playing) { + if (!_neverEnabled && _enabled) { + _running = true; + _anim->start(); + } + + return _running; + } + + return LBItem::togglePlaying(playing); +} + +void LBAnimationItem::done(bool onlyNotify) { + if (!onlyNotify) { + _anim->stop(); + } + + LBItem::done(onlyNotify); +} + +void LBAnimationItem::init() { + _anim = new LBAnimation(_vm, this, _resourceId); +} + +void LBAnimationItem::stop() { + if (_running) { + _anim->stop(); + seek(0xFFFF); + } + + // TODO: handle this properly + _vm->_sound->stopSound(); + + _running = false; + + LBItem::stop(); +} + +void LBAnimationItem::seek(uint16 pos) { + _anim->seek(pos); +} + +void LBAnimationItem::startPhase(uint phase) { + if (phase == _phase) + seek(1); + + LBItem::startPhase(phase); +} + +void LBAnimationItem::draw() { + if (!_visible) + return; + + _anim->draw(); +} + } // End of namespace Mohawk diff --git a/engines/mohawk/livingbooks.h b/engines/mohawk/livingbooks.h index 0305c92c6a..0ad99e00df 100644 --- a/engines/mohawk/livingbooks.h +++ b/engines/mohawk/livingbooks.h @@ -32,14 +32,355 @@ #include "common/config-file.h" #include "common/substream.h" +#include "common/rect.h" +#include "common/queue.h" +#include "common/random.h" + +#include "sound/mixer.h" namespace Mohawk { +enum NodeState { + kLBNodeDone = 0, + kLBNodeRunning = 1, + kLBNodeWaiting = 2 +}; + +enum LBMode { + kLBIntroMode = 1, + kLBControlMode = 2, + kLBCreditsMode = 3, + kLBPreviewMode = 4, + kLBReadMode = 5, + kLBPlayMode = 6 +}; + +enum { + kLBStaticTextItem = 0x1, + kLBPictureItem = 0x2, + kLBEditTextItem = 0x14, + kLBLiveTextItem = 0x15, + kLBAnimationItem = 0x40, + kLBSoundItem = 0x41, + kLBGroupItem = 0x42, + kLBPaletteItem = 0x45 // v3 +}; + enum { - kIntroPage = 0 + // no 0x1? + kLBAnimOpNotify = 0x2, + kLBAnimOpSetTempo = 0x3, + // no 0x4? + kLBAnimOpMoveTo = 0x5, + kLBAnimOpWait = 0x6, + kLBAnimOpSetCel = 0x7, + kLBAnimOpSleepUntil = 0x8, + kLBAnimOpDrawMode = 0x9, + kLBAnimOpPlaySound = 0xa, + kLBAnimOpWaitForSound = 0xb, + kLBAnimOpReleaseSound = 0xc, + kLBAnimOpResetSound = 0xd, + kLBAnimOpUnknownE = 0xe }; +enum { + kLBActionPhase0 = 0, + kLBActionPhase1 = 1, + kLBActionMouseDown = 2, + kLBActionStarted = 3, + kLBActionDone = 4, + kLBActionMouseUp = 5, + kLBActionPhase2 = 6 +}; + +enum { + kLBGroupData = 0x64, + kLBLiveTextData = 0x65, + kLBMsgListScript = 0x66, + kLBNotifyScript = 0x67, + kLBSetPlayInfo = 0x68, + kLBSetPlayPhase = 0x6e +}; + +enum { + kLBNotifyGUIAction = 1, + kLBNotifyGoToControls = 2, + kLBNotifyChangePage = 3, + kLBNotifyIntroDone = 5, + kLBNotifyQuit = 6, + kLBNotifyCursorChange = 7 +}; + +class MohawkEngine_LivingBooks; class LBGraphics; +class LBAnimation; + +struct LBScriptEntry { + LBScriptEntry(); + ~LBScriptEntry(); + + uint16 type; + uint16 action; + uint16 opcode; + uint16 param; + uint16 argc; + uint16 *argvParam; + uint16 *argvTarget; +}; + +struct LBAnimScriptEntry { + byte opcode; + byte size; + byte *data; +}; + +class LBAnimationNode { +public: + LBAnimationNode(MohawkEngine_LivingBooks *vm, LBAnimation *parent, uint16 scriptResourceId); + ~LBAnimationNode(); + + void draw(const Common::Rect &_bounds); + void reset(); + NodeState update(bool seeking = false); + bool transparentAt(int x, int y); + +protected: + MohawkEngine_LivingBooks *_vm; + LBAnimation *_parent; + + void loadScript(uint16 resourceId); + uint _currentEntry; + Common::Array _scriptEntries; + + uint _currentCel; + int16 _xPos, _yPos; +}; + +class LBAnimationItem; + +class LBAnimation { +public: + LBAnimation(MohawkEngine_LivingBooks *vm, LBAnimationItem *parent, uint16 resourceId); + ~LBAnimation(); + + void draw(); + void update(); + + void start(); + void seek(uint16 pos); + void stop(); + + bool wasDone(); + bool transparentAt(int x, int y); + + void setTempo(uint16 tempo); + + uint getNumResources() { return _shapeResources.size(); } + uint16 getResource(uint num) { return _shapeResources[num]; } + Common::Point getOffset(uint num) { return _shapeOffsets[num]; } + + uint32 getCurrentFrame() { return _currentFrame; } + + uint16 getParentId(); + +protected: + MohawkEngine_LivingBooks *_vm; + LBAnimationItem *_parent; + + Common::Rect _bounds, _clip; + Common::Array _nodes; + + uint16 _tempo; + uint32 _lastTime, _currentFrame; + bool _running, _done; + + void loadShape(uint16 resourceId); + Common::Array _shapeResources; + Common::Array _shapeOffsets; +}; + +class LBItem { +public: + LBItem(MohawkEngine_LivingBooks *vm, Common::Rect rect); + virtual ~LBItem(); + + void readFrom(Common::SeekableSubReadStreamEndian *stream); + virtual void readData(uint16 type, uint16 size, Common::SeekableSubReadStreamEndian *stream); + + virtual void destroySelf(); // 0x2 + virtual void setEnabled(bool enabled); // 0x3 + virtual bool contains(Common::Point point); // 0x7 + virtual void update(); // 0x8 + virtual void draw() { } // 0x9 + virtual void handleKeyChar(Common::Point pos) { } // 0xA + virtual void handleMouseDown(Common::Point pos); // 0xB + virtual void handleMouseMove(Common::Point pos); // 0xC + virtual void handleMouseUp(Common::Point pos); // 0xD + virtual bool togglePlaying(bool playing); // 0xF + virtual void done(bool onlyNotify); // 0x10 + virtual void init() { } // 0x11 + virtual void seek(uint16 pos) { } // 0x13 + virtual void setFocused(bool focused) { } // 0x14 + virtual void setVisible(bool visible); // 0x17 + virtual void startPhase(uint phase); // 0x18 + virtual void stop(); // 0x19 + virtual void notify(uint16 data, uint16 from); // 0x1A + + uint16 getId() { return _itemId; } + +protected: + MohawkEngine_LivingBooks *_vm; + + void setNextTime(uint16 min, uint16 max); + void setNextTime(uint16 min, uint16 max, uint32 start); + + Common::Rect _rect; + Common::String _desc; + uint16 _resourceId; + uint16 _itemId; + + bool _visible, _playing, _enabled, _neverEnabled; + + uint32 _nextTime, _startTime; + uint16 _loops; + + uint16 _phase, _timingMode, _delayMin, _delayMax; + uint16 _loopMode, _loopCount, _periodMin, _periodMax; + uint16 _controlMode; + Common::Point _relocPoint; + + Common::Array _scriptEntries; + void runScript(uint id); +}; + +class LBSoundItem : public LBItem { +public: + LBSoundItem(MohawkEngine_LivingBooks *_vm, Common::Rect rect); + ~LBSoundItem(); + + bool togglePlaying(bool playing); + void stop(); +}; + +struct GroupEntry { + uint entryId; + uint entryType; +}; + +class LBGroupItem : public LBItem { +public: + LBGroupItem(MohawkEngine_LivingBooks *_vm, Common::Rect rect); + + void readData(uint16 type, uint16 size, Common::SeekableSubReadStreamEndian *stream); + + void setEnabled(bool enabled); + bool contains(Common::Point point); + bool togglePlaying(bool playing); + // 0x12 + void seek(uint16 pos); + void setVisible(bool visible); + void startPhase(uint phase); + void stop(); + +protected: + bool _starting; + + Common::Array _groupEntries; +}; + +class LBPaletteItem : public LBItem { +public: + LBPaletteItem(MohawkEngine_LivingBooks *_vm, Common::Rect rect); + + void readData(uint16 type, uint16 size, Common::SeekableSubReadStreamEndian *stream); + + void startPhase(uint phase); + +protected: + uint16 _start, _count; + byte _palette[256 * 4]; +}; + +struct LiveTextWord { + Common::Rect bounds; + uint16 soundId; +}; + +struct LiveTextPhrase { + uint16 wordStart, wordCount; + uint16 highlightStart, highlightEnd; + uint16 startId, endId; +}; + +class LBLiveTextItem : public LBItem { +public: + LBLiveTextItem(MohawkEngine_LivingBooks *_vm, Common::Rect rect); + + void readData(uint16 type, uint16 size, Common::SeekableSubReadStreamEndian *stream); + + void notify(uint16 data, uint16 from); + +protected: + bool _running; + + byte _backgroundColor[4]; + byte _foregroundColor[4]; + byte _highlightColor[4]; + uint16 _paletteIndex; + + Common::Array _words; + Common::Array _phrases; +}; + +class LBPictureItem : public LBItem { +public: + LBPictureItem(MohawkEngine_LivingBooks *_vm, Common::Rect rect); + + void readData(uint16 type, uint16 size, Common::SeekableSubReadStreamEndian *stream); + + bool contains(Common::Point point); + void draw(); + void init(); +}; + +class LBAnimationItem : public LBItem { +public: + LBAnimationItem(MohawkEngine_LivingBooks *_vm, Common::Rect rect); + ~LBAnimationItem(); + + void setEnabled(bool enabled); + bool contains(Common::Point point); + void update(); + void draw(); + bool togglePlaying(bool playing); + void done(bool onlyNotify); + void init(); + void seek(uint16 pos); + void startPhase(uint phase); + void stop(); + +protected: + LBAnimation *_anim; + bool _running; +}; + +struct NotifyEvent { + NotifyEvent(uint t, uint p) : type(t), param(p) { } + uint type; + uint param; +}; + +enum DelayedEventType { + kLBDestroy = 0, + kLBSetNotVisible = 1, + kLBDone = 2 +}; + +struct DelayedEvent { + DelayedEvent(LBItem *i, DelayedEventType t) : item(i), type(t) { } + LBItem *item; + DelayedEventType type; +}; class MohawkEngine_LivingBooks : public MohawkEngine { protected: @@ -49,24 +390,54 @@ public: MohawkEngine_LivingBooks(OSystem *syst, const MohawkGameDescription *gamedesc); virtual ~MohawkEngine_LivingBooks(); + Common::RandomSource *_rnd; + LBGraphics *_gfx; - bool _needsUpdate; + bool _needsRedraw, _needsUpdate; + + void addNotifyEvent(NotifyEvent event); Common::SeekableSubReadStreamEndian *wrapStreamEndian(uint32 tag, uint16 id); + Common::Rect readRect(Common::SeekableSubReadStreamEndian *stream); GUI::Debugger *getDebugger() { return _console; } + LBItem *getItemById(uint16 id); + + void setFocus(LBItem *focus); + void setEnableForAll(bool enable, LBItem *except = 0); + void notifyAll(uint16 data, uint16 from); + void queueDelayedEvent(DelayedEvent event); + + bool isBigEndian() const { return getGameType() == GType_LIVINGBOOKSV3 || getPlatform() == Common::kPlatformMacintosh; } + private: LivingBooksConsole *_console; Common::ConfigFile _bookInfoFile; - uint16 _curPage; Common::String getBookInfoFileName() const; void loadBookInfo(const Common::String &filename); - void loadIntro(); + + Common::String stringForMode(LBMode mode); + + bool _readOnly, _introDone; + LBMode _curMode; + uint16 _curPage, _curSubPage; + uint16 _phase; + Common::Array _items; + Common::Queue _eventQueue; + LBItem *_focus; + void destroyPage(); + bool loadPage(LBMode mode, uint page, uint subpage); + void updatePage(); uint16 getResourceVersion(); + void loadBITL(uint16 resourceId); void loadSHP(uint16 resourceId); - void loadANI(uint16 resourceId); + + void nextPage(); + + Common::Queue _notifyEvents; + void handleNotify(NotifyEvent &event); uint16 _screenWidth; uint16 _screenHeight; @@ -74,6 +445,10 @@ private: uint16 _numPages; Common::String _title; Common::String _copyright; + bool _poetryMode; + + uint16 _curLanguage; + bool _alreadyShowedIntro; // String Manipulation Functions Common::String removeQuotesFromString(const Common::String &string); @@ -86,7 +461,6 @@ private: Common::String getFileNameFromConfig(const Common::String §ion, const Common::String &key); // Platform/Version functions - bool isBigEndian() const { return getGameType() == GType_LIVINGBOOKSV3 || getPlatform() == Common::kPlatformMacintosh; } MohawkArchive *createMohawkArchive() const; }; diff --git a/engines/mohawk/mohawk.h b/engines/mohawk/mohawk.h index ed59f727f3..a1027ce066 100644 --- a/engines/mohawk/mohawk.h +++ b/engines/mohawk/mohawk.h @@ -68,7 +68,8 @@ enum MohawkGameFeatures { GF_ME = (1 << 0), // Myst Masterpiece Edition GF_DVD = (1 << 1), GF_DEMO = (1 << 2), - GF_HASMIDI = (1 << 3) + GF_HASMIDI = (1 << 3), + GF_NO_READONLY = (1 << 4) // very early Living Books games }; struct MohawkGameDescription; -- cgit v1.2.3