[Scummvm-cvs-logs] SF.net SVN: scummvm:[54558] scummvm/trunk/engines/mohawk

mthreepwood at users.sourceforge.net mthreepwood at users.sourceforge.net
Sun Nov 28 23:55:16 CET 2010


Revision: 54558
          http://scummvm.svn.sourceforge.net/scummvm/?rev=54558&view=rev
Author:   mthreepwood
Date:     2010-11-28 22:55:15 +0000 (Sun, 28 Nov 2010)

Log Message:
-----------
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.

Modified Paths:
--------------
    scummvm/trunk/engines/mohawk/console.cpp
    scummvm/trunk/engines/mohawk/cursors.cpp
    scummvm/trunk/engines/mohawk/cursors.h
    scummvm/trunk/engines/mohawk/detection_tables.h
    scummvm/trunk/engines/mohawk/graphics.cpp
    scummvm/trunk/engines/mohawk/graphics.h
    scummvm/trunk/engines/mohawk/livingbooks.cpp
    scummvm/trunk/engines/mohawk/livingbooks.h
    scummvm/trunk/engines/mohawk/mohawk.h

Modified: scummvm/trunk/engines/mohawk/console.cpp
===================================================================
--- scummvm/trunk/engines/mohawk/console.cpp	2010-11-28 22:55:00 UTC (rev 54557)
+++ scummvm/trunk/engines/mohawk/console.cpp	2010-11-28 22:55:15 UTC (rev 54558)
@@ -690,6 +690,7 @@
 	}
 
 	_vm->_gfx->copyImageToScreen((uint16)atoi(argv[1]));
+	_vm->_system->updateScreen();
 	return false;
 }
 

Modified: scummvm/trunk/engines/mohawk/cursors.cpp
===================================================================
--- scummvm/trunk/engines/mohawk/cursors.cpp	2010-11-28 22:55:00 UTC (rev 54557)
+++ scummvm/trunk/engines/mohawk/cursors.cpp	2010-11-28 22:55:15 UTC (rev 54558)
@@ -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 @@
 	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 @@
 	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 @@
 	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<Common::NECursorGroup> 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

Modified: scummvm/trunk/engines/mohawk/cursors.h
===================================================================
--- scummvm/trunk/engines/mohawk/cursors.h	2010-11-28 22:55:00 UTC (rev 54557)
+++ scummvm/trunk/engines/mohawk/cursors.h	2010-11-28 22:55:15 UTC (rev 54558)
@@ -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 @@
 	kRivenHideCursor = 9000
 };
 
+class MohawkEngine;
 class MohawkEngine_Myst;
 class MystBitmap;
 
@@ -67,9 +75,29 @@
 
 	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,20 +106,49 @@
 	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
 
 #endif

Modified: scummvm/trunk/engines/mohawk/detection_tables.h
===================================================================
--- scummvm/trunk/engines/mohawk/detection_tables.h	2010-11-28 22:55:00 UTC (rev 54557)
+++ scummvm/trunk/engines/mohawk/detection_tables.h	2010-11-28 22:55:15 UTC (rev 54558)
@@ -605,6 +605,21 @@
 	{
 		{
 			"tortoise",
+			"",
+			AD_ENTRY1("TORTOISE.512", "dfcf7bff3d0f187832c9897497efde0e"),
+			Common::EN_ANY,
+			Common::kPlatformWindows,
+			ADGF_NO_FLAGS,
+			Common::GUIO_NONE
+		},
+		GType_LIVINGBOOKSV1,
+		0,
+		"TORTOISE.EXE"
+	},
+
+	{
+		{
+			"tortoise",
 			"Demo v1.0",
 			AD_ENTRY1("TORTOISE.512", "75d9a2f8339e423604a0c6e8177600a6"),
 			Common::EN_ANY,
@@ -658,7 +673,7 @@
 			Common::GUIO_NONE
 		},
 		GType_LIVINGBOOKSV1,
-		0,
+		GF_NO_READONLY,
 		"ARTHUR.EXE" // FIXME: Check this (ST?) 
 	},
 
@@ -673,7 +688,7 @@
 			Common::GUIO_NONE
 		},
 		GType_LIVINGBOOKSV1,
-		GF_DEMO,
+		GF_DEMO | GF_NO_READONLY,
 		"ARTHUR.EXE"
 	},
 
@@ -733,7 +748,7 @@
 			Common::GUIO_NONE
 		},
 		GType_LIVINGBOOKSV1,
-		GF_DEMO,
+		GF_DEMO | GF_NO_READONLY,
 		"GRANDMA.EXE"
 	},
 
@@ -763,7 +778,7 @@
 			Common::GUIO_NONE
 		},
 		GType_LIVINGBOOKSV1,
-		GF_DEMO,
+		GF_DEMO | GF_NO_READONLY,
 		"Just Grandma and Me"
 	},
 

Modified: scummvm/trunk/engines/mohawk/graphics.cpp
===================================================================
--- scummvm/trunk/engines/mohawk/graphics.cpp	2010-11-28 22:55:00 UTC (rev 54557)
+++ scummvm/trunk/engines/mohawk/graphics.cpp	2010-11-28 22:55:15 UTC (rev 54558)
@@ -654,8 +654,10 @@
 	_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,17 +671,84 @@
 	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<int>(surface->w, _vm->_system->getWidth());
-	uint16 height = MIN<int>(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<int>(surface->w - startX, _vm->_system->getWidth() - left);
+	uint16 height = MIN<int>(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) {
 	// Old Living Books games use the old CTBL-style palette format while newer
 	// games use the better tPAL format which can store partial palettes.

Modified: scummvm/trunk/engines/mohawk/graphics.h
===================================================================
--- scummvm/trunk/engines/mohawk/graphics.h	2010-11-28 22:55:00 UTC (rev 54557)
+++ scummvm/trunk/engines/mohawk/graphics.h	2010-11-28 22:55:15 UTC (rev 54558)
@@ -196,11 +196,13 @@
 
 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);

Modified: scummvm/trunk/engines/mohawk/livingbooks.cpp
===================================================================
--- scummvm/trunk/engines/mohawk/livingbooks.cpp	2010-11-28 22:55:00 UTC (rev 54557)
+++ scummvm/trunk/engines/mohawk/livingbooks.cpp	2010-11-28 22:55:15 UTC (rev 54558)
@@ -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 @@
 	MohawkEngine::run();
 
 	_console = new LivingBooksConsole(this);
-	_gfx = new LBGraphics(this);
-
 	// Load the book info from the detected file
 	loadBookInfo(getBookInfoFileName());
 
@@ -60,33 +103,43 @@
 	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();
+	_cursor->setDefaultCursor();
+	_cursor->showCursor();
 
-	debug(1, "Stack Version: %d", getResourceVersion());
+	if (!loadPage(kLBIntroMode, 1, 0))
+		error("Could not load intro page");
 
-	_gfx->setPalette(1000);
-	loadSHP(1000);
-	loadANI(1000);
-
-	// Code to Load Sounds For Debugging...
-	//for (byte i = 0; i < 30; i++)
-	//	_sound->playSound(1000+i);
-
 	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 @@
 						_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 @@
 
 		// 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 @@
 	_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 @@
 	//     - fDebugWindow                (always 0?)
 }
 
-void MohawkEngine_LivingBooks::loadIntro() {
+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();
+	_gfx->clearCache();
+
+	_eventQueue.clear();
+
+	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;
 
-	// We get to try for a few different names! Yay!
-	filename = getFileNameFromConfig("Intro", "Page1");
+	filename = getFileNameFromConfig(name, base);
+	_readOnly = false;
 
-	// Some store with .r, not sure why.
-	if (filename.empty())
-		filename = getFileNameFromConfig("Intro", "Page1.r");
+	if (filename.empty()) {
+		filename = getFileNameFromConfig(name, base + ".r");
+		_readOnly = true;
+	}
 
-	if (!filename.empty()) {
-		MohawkArchive *introArchive = createMohawkArchive();
-		if (introArchive->open(filename))
-			_mhk.push_back(introArchive);
-		else
-			delete introArchive;
+	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);
+		}
 	}
 
-	filename = getFileNameFromConfig("Intro", "Page2");
+	// 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())
-		filename = getFileNameFromConfig("Intro", "Page2.r");
-
-	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 (shpStream->size() < 6)
-		warning("SHP Record size too short");
+		if (_curMode == kLBControlMode) {
+			// hard-coded control page startup
+			LBItem *item;
 
-	if (shpStream->readUint16() != 3)
-		warning("SHP Record u0 not 3");
+			item = getItemById(10);
+			if (item)
+				item->togglePlaying(false);
 
-	if (shpStream->readUint16() != 0)
-		warning("SHP Record u1 not 0");
+			switch (_curPage) {
+			case 1:
+				debug(2, "updatePage() for control page 1");
 
-	uint16 idCount = shpStream->readUint16();
-	debug(1, "SHP: idCount: %d", idCount);
+				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);
+				}
 
-	if (shpStream->size() != (idCount * 2) + 6)
-		warning("SHP Record size mismatch");
+				item = getItemById(12);
+				if (item)
+					item->setVisible(false);
 
-	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]);
+				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);
+
+		_phase++;
+		break;
+
+	case 2:
+		if (!_introDone)
+			break;
+
+		for (uint32 i = 0; i < _items.size(); i++)
+			_items[i]->startPhase(_phase);
+
+		_phase++;
+		break;
 	}
 
-	delete[] idValues;
-	delete shpStream;
+	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;
+			}
+
+			break;
+		}
+	}
+
+	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?
+void MohawkEngine_LivingBooks::setFocus(LBItem *focus) {
+	_focus = focus;
+}
 
-	uint16 u1 = aniStream->readUint16();
-	debug(1, "ANI u1: %d", u1);
+void MohawkEngine_LivingBooks::setEnableForAll(bool enable, LBItem *except) {
+	for (uint16 i = 0; i < _items.size(); i++)
+		if (except != _items[i])
+			_items[i]->setEnabled(enable);
+}
 
-	uint16 u2 = aniStream->readUint16();
-	debug(1, "ANI u2: %d", u2);
+void MohawkEngine_LivingBooks::notifyAll(uint16 data, uint16 from) {
+	for (uint16 i = 0; i < _items.size(); i++)
+		_items[i]->notify(data, from);
+}
 
-	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);
+void MohawkEngine_LivingBooks::queueDelayedEvent(DelayedEvent event) {
+	_eventQueue.push(event);
+}
 
-	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);
+// Only 1 VSRN resource per stack, Id 1000
+uint16 MohawkEngine_LivingBooks::getResourceVersion() {
+	Common::SeekableReadStream *versionStream = getResource(ID_VRSN, 1000);
 
-	// BMAP Id?
-	uint16 u4ResourceId = aniStream->readUint16();
-	debug(1, "ANI u4ResourceId: %d", u4ResourceId);
+	if (versionStream->size() != 2)
+		warning("Version Record size mismatch");
 
-	// 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 version = versionStream->readUint16BE();
 
-	uint16 u6 = aniStream->readUint16();
-	debug(1, "ANI u6: %d", u6);
-	if (u6 != 0)
-		warning("ANI u6 non-zero");
+	delete versionStream;
+	return version;
+}
 
-	uint16 u7 = aniStream->readUint16();
-	debug(1, "ANI u7: %d", u7);
-	if (u7 != 0)
-		warning("ANI u7 non-zero");
+void MohawkEngine_LivingBooks::loadBITL(uint16 resourceId) {
+	Common::SeekableSubReadStreamEndian *bitlStream = wrapStreamEndian(ID_BITL, resourceId);
 
-	delete aniStream;
+	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::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::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 @@
 	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<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]));
+
+	_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

Modified: scummvm/trunk/engines/mohawk/livingbooks.h
===================================================================
--- scummvm/trunk/engines/mohawk/livingbooks.h	2010-11-28 22:55:00 UTC (rev 54557)
+++ scummvm/trunk/engines/mohawk/livingbooks.h	2010-11-28 22:55:15 UTC (rev 54558)
@@ -32,15 +32,356 @@
 
 #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 {
-	kIntroPage = 0
+	kLBStaticTextItem = 0x1,
+	kLBPictureItem = 0x2,
+	kLBEditTextItem = 0x14,
+	kLBLiveTextItem = 0x15,
+	kLBAnimationItem = 0x40,
+	kLBSoundItem = 0x41,
+	kLBGroupItem = 0x42,
+	kLBPaletteItem = 0x45 // v3
 };
 
+enum {
+	// 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<LBAnimScriptEntry> _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<LBAnimationNode *> _nodes;
+
+	uint16 _tempo;
+	uint32 _lastTime, _currentFrame;
+	bool _running, _done;
+
+	void loadShape(uint16 resourceId);
+	Common::Array<uint16> _shapeResources;
+	Common::Array<Common::Point> _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<LBScriptEntry *> _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<GroupEntry> _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<LiveTextWord> _words;
+	Common::Array<LiveTextPhrase> _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:
 	Common::Error run();
@@ -49,32 +390,66 @@
 	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<LBItem *> _items;
+	Common::Queue<DelayedEvent> _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<NotifyEvent> _notifyEvents;
+	void handleNotify(NotifyEvent &event);
+
 	uint16 _screenWidth;
 	uint16 _screenHeight;
 	uint16 _numLanguages;
 	uint16 _numPages;
 	Common::String _title;
 	Common::String _copyright;
+	bool _poetryMode;
 
+	uint16 _curLanguage;
+	bool _alreadyShowedIntro;
+
 	// String Manipulation Functions
 	Common::String removeQuotesFromString(const Common::String &string);
 	Common::String convertMacFileName(const Common::String &string);
@@ -86,7 +461,6 @@
 	Common::String getFileNameFromConfig(const Common::String &section, const Common::String &key);
 
 	// Platform/Version functions
-	bool isBigEndian() const { return getGameType() == GType_LIVINGBOOKSV3 || getPlatform() == Common::kPlatformMacintosh; }
 	MohawkArchive *createMohawkArchive() const;
 };
 

Modified: scummvm/trunk/engines/mohawk/mohawk.h
===================================================================
--- scummvm/trunk/engines/mohawk/mohawk.h	2010-11-28 22:55:00 UTC (rev 54557)
+++ scummvm/trunk/engines/mohawk/mohawk.h	2010-11-28 22:55:15 UTC (rev 54558)
@@ -68,7 +68,8 @@
 	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;


This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site.




More information about the Scummvm-git-logs mailing list