diff options
-rw-r--r-- | base/plugins.cpp | 3 | ||||
-rwxr-xr-x | configure | 1 | ||||
-rw-r--r-- | engines/composer/composer.cpp | 495 | ||||
-rw-r--r-- | engines/composer/composer.h | 212 | ||||
-rw-r--r-- | engines/composer/detection.cpp | 179 | ||||
-rw-r--r-- | engines/composer/graphics.cpp | 758 | ||||
-rw-r--r-- | engines/composer/graphics.h | 71 | ||||
-rw-r--r-- | engines/composer/module.mk | 16 | ||||
-rw-r--r-- | engines/composer/resource.cpp | 336 | ||||
-rw-r--r-- | engines/composer/resource.h | 124 | ||||
-rw-r--r-- | engines/composer/scripting.cpp | 733 | ||||
-rw-r--r-- | engines/engines.mk | 5 |
12 files changed, 2933 insertions, 0 deletions
diff --git a/base/plugins.cpp b/base/plugins.cpp index 4413995b88..4fa1a961da 100644 --- a/base/plugins.cpp +++ b/base/plugins.cpp @@ -97,6 +97,9 @@ public: #if PLUGIN_ENABLED_STATIC(CINE) LINK_PLUGIN(CINE) #endif + #if PLUGIN_ENABLED_STATIC(COMPOSER) + LINK_PLUGIN(COMPOSER) + #endif #if PLUGIN_ENABLED_STATIC(CRUISE) LINK_PLUGIN(CRUISE) #endif @@ -84,6 +84,7 @@ add_engine agi "AGI" yes add_engine agos "AGOS" yes "agos2" add_engine agos2 "AGOS 2 games" yes add_engine cine "Cinematique evo 1" yes +add_engine composer "Magic Composer" no add_engine cruise "Cinematique evo 2" yes add_engine draci "Dragon History" yes add_engine drascula "Drascula: The Vampire Strikes Back" yes diff --git a/engines/composer/composer.cpp b/engines/composer/composer.cpp new file mode 100644 index 0000000000..cf773dc2dc --- /dev/null +++ b/engines/composer/composer.cpp @@ -0,0 +1,495 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ +#include "common/scummsys.h" + +#include "common/config-manager.h" +#include "common/events.h" +#include "common/file.h" +#include "common/random.h" +#include "common/fs.h" +#include "common/keyboard.h" +#include "common/substream.h" + +#include "graphics/cursorman.h" +#include "graphics/surface.h" +#include "graphics/pixelformat.h" + +#include "engines/util.h" +#include "engines/advancedDetector.h" + +#include "audio/audiostream.h" + +#include "composer/composer.h" +#include "composer/graphics.h" +#include "composer/resource.h" + +namespace Composer { + +ComposerEngine::ComposerEngine(OSystem *syst, const ComposerGameDescription *gameDesc) : Engine(syst), _gameDescription(gameDesc) { + _rnd = new Common::RandomSource("composer"); + _audioStream = NULL; +} + +ComposerEngine::~ComposerEngine() { + DebugMan.clearAllDebugChannels(); + + for (Common::List<Library>::iterator i = _libraries.begin(); i != _libraries.end(); i++) + delete i->_archive; + for (Common::List<Sprite>::iterator i = _sprites.begin(); i != _sprites.end(); i++) + i->_surface.free(); + + delete _rnd; +} + +Common::Error ComposerEngine::run() { + Common::Event event; + + _vars.resize(1000); + for (uint i = 0; i < _vars.size(); i++) + _vars[i] = 0; + + _queuedScripts.resize(10); + for (uint i = 0; i < _queuedScripts.size(); i++) { + _queuedScripts[i]._count = 0; + _queuedScripts[i]._scriptId = 0; + } + + _mouseVisible = true; + _mouseEnabled = false; + _mouseSpriteId = 0; + _lastButton = NULL; + + _directoriesToStrip = 1; + if (!_bookIni.loadFromFile("book.ini")) { + _directoriesToStrip = 0; + if (!_bookIni.loadFromFile("programs/book.ini")) + error("failed to find book.ini"); + } + + uint width = 640; + if (_bookIni.hasKey("Width", "Common")) + width = atoi(getStringFromConfig("Common", "Width").c_str()); + uint height = 480; + if (_bookIni.hasKey("Height", "Common")) + height = atoi(getStringFromConfig("Common", "Height").c_str()); + initGraphics(width, height, true); + _surface.create(width, height, Graphics::PixelFormat::createFormatCLUT8()); + _needsUpdate = true; + + loadLibrary(0); + + uint fps = atoi(getStringFromConfig("Common", "FPS").c_str()); + uint frameTime = 1000 / fps; + uint32 lastDrawTime = 0; + while (!shouldQuit()) { + for (uint i = 0; i < _pendingPageChanges.size(); i++) { + if (_pendingPageChanges[i]._remove) + unloadLibrary(_pendingPageChanges[i]._pageId); + else + loadLibrary(_pendingPageChanges[i]._pageId); + + lastDrawTime = _system->getMillis(); + } + _pendingPageChanges.clear(); + + uint32 thisTime = _system->getMillis(); + for (uint i = 0; i < _queuedScripts.size(); i++) { + QueuedScript &script = _queuedScripts[i]; + if (!script._count) + continue; + if (script._baseTime + script._duration > thisTime) + continue; + if (script._count != 0xffffffff) + script._count--; + script._baseTime = thisTime; + runScript(script._scriptId, i, 0, 0); + } + + if (lastDrawTime + frameTime <= thisTime) { + // catch up if we're more than 5 frames behind + if (lastDrawTime + (frameTime * 5) <= thisTime) + lastDrawTime = thisTime - (frameTime * 5); + else + lastDrawTime += frameTime; + + redraw(); + + processAnimFrame(); + } else if (_needsUpdate) { + redraw(); + } + + while (_eventMan->pollEvent(event)) { + switch (event.type) { + case Common::EVENT_LBUTTONDOWN: + onMouseDown(event.mouse); + break; + + case Common::EVENT_LBUTTONUP: + break; + + case Common::EVENT_RBUTTONDOWN: + break; + + case Common::EVENT_MOUSEMOVE: + onMouseMove(event.mouse); + break; + + case Common::EVENT_KEYDOWN: + switch (event.kbd.keycode) { + case Common::KEYCODE_d: + /*if (event.kbd.hasFlags(Common::KBD_CTRL)) { + // Start the debugger + getDebugger()->attach(); + getDebugger()->onFrame(); + }*/ + break; + + case Common::KEYCODE_q: + if (event.kbd.hasFlags(Common::KBD_CTRL)) + quitGame(); + break; + + default: + break; + } + + onKeyDown(event.kbd.keycode); + break; + + case Common::EVENT_QUIT: + case Common::EVENT_RTL: + quitGame(); + break; + + default: + break; + } + } + + _system->delayMillis(20); + } + + return Common::kNoError; +} + +void ComposerEngine::onMouseDown(const Common::Point &pos) { + if (!_mouseEnabled || !_mouseVisible) + return; + + const Sprite *sprite = getSpriteAtPos(pos); + const Button *button = getButtonFor(sprite, pos); + if (!button) + return; + + // TODO: other buttons? + uint16 buttonsDown = 1; // MK_LBUTTON + + uint16 spriteId = sprite ? sprite->_id : 0; + runScript(button->_scriptId, button->_id, buttonsDown, spriteId); +} + +void ComposerEngine::onMouseMove(const Common::Point &pos) { + _lastMousePos = pos; + + if (!_mouseEnabled || !_mouseVisible) + return; + + // TODO: do we need to keep track of this? + uint buttonsDown = 0; + + const Sprite *sprite = getSpriteAtPos(pos); + const Button *button = getButtonFor(sprite, pos); + if (_lastButton != button) { + if (_lastButton && _lastButton->_scriptIdRollOff) + runScript(_lastButton->_scriptIdRollOff, _lastButton->_id, buttonsDown, 0); + _lastButton = button; + if (_lastButton && _lastButton->_scriptIdRollOn) + runScript(_lastButton->_scriptIdRollOn, _lastButton->_id, buttonsDown, 0); + } + + if (_mouseSpriteId) { + addSprite(_mouseSpriteId, 0, 0, _lastMousePos - _mouseOffset); + _needsUpdate = true; + } +} + +void ComposerEngine::onKeyDown(uint16 keyCode) { + runEvent(kEventKeyDown, keyCode, 0, 0); + runEvent(kEventChar, keyCode, 0, 0); +} + +void ComposerEngine::setCursor(uint16 id, const Common::Point &offset) { + _mouseOffset = offset; + if (_mouseSpriteId == id) + return; + + if (_mouseSpriteId && _mouseVisible) { + removeSprite(_mouseSpriteId, 0); + } + _mouseSpriteId = id; + if (_mouseSpriteId && _mouseVisible) { + addSprite(_mouseSpriteId, 0, 0, _lastMousePos - _mouseOffset); + } +} + +void ComposerEngine::setCursorVisible(bool visible) { + if (!_mouseSpriteId) + return; + + if (visible && !_mouseVisible) { + _mouseVisible = true; + addSprite(_mouseSpriteId, 0, 0, _lastMousePos - _mouseOffset); + onMouseMove(_lastMousePos); + } else if (!visible && _mouseVisible) { + _mouseVisible = false; + removeSprite(_mouseSpriteId, 0); + } +} + +Common::String ComposerEngine::getStringFromConfig(const Common::String §ion, const Common::String &key) { + Common::String value; + if (!_bookIni.getKey(key, section, value)) + error("failed to find key '%s' in section '%s' of book config", key.c_str(), section.c_str()); + return value; +} + +Common::String ComposerEngine::getFilename(const Common::String §ion, uint id) { + Common::String key = Common::String::format("%d", id); + Common::String filename = getStringFromConfig(section, key); + while (filename.size() && (filename[0] == '~' || filename[0] == ':' || filename[0] == '\\')) + filename = filename.c_str() + 1; + + uint slashesToStrip = _directoriesToStrip; + while (slashesToStrip--) { + for (uint i = 0; i < filename.size(); i++) { + if (filename[i] != '\\') + continue; + filename = filename.c_str() + i + 1; + break; + } + } + + Common::String outFilename; + for (uint i = 0; i < filename.size(); i++) { + if (filename[i] == '\\') + outFilename += '/'; + else + outFilename += filename[i]; + } + return outFilename; +} + +void ComposerEngine::loadLibrary(uint id) { + if (!id) + id = atoi(getStringFromConfig("Common", "StartUp").c_str()); + Common::String filename = getFilename("Libs", id); + + Library library; + + library._id = id; + library._archive = new ComposerArchive(); + if (!library._archive->openFile(filename)) + error("failed to open '%s'", filename.c_str()); + _libraries.push_front(library); + + Library &newLib = _libraries.front(); + + Common::Array<uint16> buttonResources = library._archive->getResourceIDList(ID_BUTN); + for (uint i = 0; i < buttonResources.size(); i++) { + uint16 buttonId = buttonResources[i]; + Common::SeekableReadStream *stream = library._archive->getResource(ID_BUTN, buttonId); + Button button(stream, buttonId); + + bool inserted = false; + for (Common::List<Button>::iterator b = newLib._buttons.begin(); b != newLib._buttons.end(); b++) { + if (button._zorder <= b->_zorder) + continue; + newLib._buttons.insert(b, button); + inserted = true; + break; + } + if (!inserted) + newLib._buttons.push_back(button); + } + + // add background sprite, if it exists + if (hasResource(ID_BMAP, 1000)) + setBackground(1000); + + // TODO: better CTBL logic + loadCTBL(1000, 100); + + // Run the startup script. + runScript(1000, 0, 0, 0); + + _mouseEnabled = true; + onMouseMove(_lastMousePos); + + runEvent(kEventLoad, id, 0, 0); +} + +void ComposerEngine::unloadLibrary(uint id) { + for (Common::List<Library>::iterator i = _libraries.begin(); i != _libraries.end(); i++) { + if (i->_id != id) + continue; + + for (Common::List<Animation *>::iterator j = _anims.begin(); j != _anims.end(); j++) { + delete *j; + } + _anims.clear(); + for (Common::List<Pipe *>::iterator j = _pipes.begin(); j != _pipes.end(); j++) { + delete *j; + } + _pipes.clear(); + + for (Common::List<Sprite>::iterator j = _sprites.begin(); j != _sprites.end(); j++) { + j->_surface.free(); + } + _sprites.clear(); + i->_buttons.clear(); + + _lastButton = NULL; + + _mixer->stopAll(); + _audioStream = NULL; + + for (uint j = 0; j < _queuedScripts.size(); j++) { + _queuedScripts[j]._count = 0; + _queuedScripts[j]._scriptId = 0; + } + + delete i->_archive; + _libraries.erase(i); + + runEvent(kEventUnload, id, 0, 0); + + return; + } + + error("tried to unload library %d, which isn't loaded", id); +} + +bool ComposerEngine::hasResource(uint32 tag, uint16 id) { + for (Common::List<Library>::iterator i = _libraries.begin(); i != _libraries.end(); i++) + if (i->_archive->hasResource(tag, id)) + return true; + + return false; +} + +Common::SeekableReadStream *ComposerEngine::getResource(uint32 tag, uint16 id) { + for (Common::List<Library>::iterator i = _libraries.begin(); i != _libraries.end(); i++) + if (i->_archive->hasResource(tag, id)) + return i->_archive->getResource(tag, id); + + error("No loaded library contains '%s' %04x", tag2str(tag), id); +} + +Button::Button(Common::SeekableReadStream *stream, uint16 id) { + _id = id; + + _type = stream->readUint16LE(); + _active = (_type & 0x8000) ? true : false; + _type &= 0xfff; + debug(9, "button: type %d, active %d", _type, _active); + + _zorder = stream->readUint16LE(); + _scriptId = stream->readUint16LE(); + _scriptIdRollOn = stream->readUint16LE(); + _scriptIdRollOff = stream->readUint16LE(); + + stream->skip(4); + + uint16 size = stream->readUint16LE(); + + switch (_type) { + case kButtonRect: + case kButtonEllipse: + if (size != 4) + error("button %d of type %d had %d points, not 4", id, _type, size); + _rect.left = stream->readSint16LE(); + _rect.top = stream->readSint16LE(); + _rect.right = stream->readSint16LE(); + _rect.bottom = stream->readSint16LE(); + debug(9, "button: (%d, %d, %d, %d)", _rect.left, _rect.top, _rect.right, _rect.bottom); + break; + case kButtonSprites: + for (uint i = 0; i < size; i++) { + _spriteIds.push_back(stream->readSint16LE()); + } + break; + default: + error("unknown button type %d", _type); + } + + delete stream; +} + +bool Button::contains(const Common::Point &pos) const { + switch (_type) { + case kButtonRect: + return _rect.contains(pos); + case kButtonEllipse: + if (!_rect.contains(pos)) + return false; + { + int16 a = _rect.width() / 2; + int16 b = _rect.height() / 2; + if (!a || !b) + return false; + Common::Point adjustedPos = pos - Common::Point(_rect.left + a, _rect.top + b); + return ((adjustedPos.x*adjustedPos.x)/(a*a) + (adjustedPos.y*adjustedPos.y)/(b*b) < 1); + } + case kButtonSprites: + return false; + default: + error("internal error (button type %d)", _type); + } +} + +const Button *ComposerEngine::getButtonFor(const Sprite *sprite, const Common::Point &pos) { + for (Common::List<Library>::iterator l = _libraries.begin(); l != _libraries.end(); l++) { + for (Common::List<Button>::iterator i = l->_buttons.reverse_begin(); i != l->_buttons.end(); --i) { + if (!i->_active) + continue; + + if (i->_spriteIds.empty()) { + if (i->contains(pos)) + return &(*i); + continue; + } + + if (!sprite) + continue; + + for (uint j = 0; j < i->_spriteIds.size(); j++) { + if (i->_spriteIds[j] == sprite->_id) + return &(*i); + } + } + } + + return NULL; +} + +} // End of namespace Composer diff --git a/engines/composer/composer.h b/engines/composer/composer.h new file mode 100644 index 0000000000..99ed56ead7 --- /dev/null +++ b/engines/composer/composer.h @@ -0,0 +1,212 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef COMPOSER_H +#define COMPOSER_H + +#include "common/config-file.h" +#include "common/random.h" +#include "common/system.h" +#include "common/debug.h" +#include "common/debug-channels.h" +#include "common/textconsole.h" +#include "common/rect.h" + +#include "engines/engine.h" +#include "engines/util.h" + +#include "graphics/surface.h" + +#include "audio/mixer.h" + +#include "composer/resource.h" + +namespace Audio { + class QueuingAudioStream; +} + +namespace Composer { + +struct ComposerGameDescription; + +enum GameType { + GType_ComposerV1, + GType_ComposerV2 +}; + +class Archive; +struct Animation; +class ComposerEngine; +class Pipe; +struct Sprite; + +enum { + kButtonRect = 0, + kButtonEllipse = 1, + kButtonSprites = 4 +}; + +class Button { +public: + Button() { } + Button(Common::SeekableReadStream *stream, uint16 id); + + bool contains(const Common::Point &pos) const; + + uint16 _id; + uint16 _type; + uint16 _zorder; + uint16 _scriptId; + uint16 _scriptIdRollOn; + uint16 _scriptIdRollOff; + bool _active; + + Common::Rect _rect; + Common::Array<uint16> _spriteIds; +}; + +enum { + kEventAnimStarted = 1, + kEventAnimDone = 2, + kEventLoad = 3, + kEventUnload = 4, + kEventKeyDown = 5, + kEventChar = 6, + kEventKeyUp = 7 +}; + +struct Library { + uint _id; + Archive *_archive; + + Common::List<Button> _buttons; +}; + +struct QueuedScript { + uint32 _baseTime; + uint32 _duration; + uint32 _count; + uint16 _scriptId; +}; + +struct PendingPageChange { + PendingPageChange() { } + PendingPageChange(uint16 id, bool remove) : _pageId(id), _remove(remove) { } + + uint16 _pageId; + bool _remove; +}; + +class ComposerEngine : public Engine { +protected: + Common::Error run(); + +public: + ComposerEngine(OSystem *syst, const ComposerGameDescription *gameDesc); + virtual ~ComposerEngine(); + + virtual bool hasFeature(EngineFeature f) const; + + int getGameType() const; + const char *getGameId() const; + uint32 getFeatures() const; + Common::Language getLanguage() const; + + const ComposerGameDescription *_gameDescription; + +private: + Common::RandomSource *_rnd; + + Audio::SoundHandle _soundHandle; + Audio::QueuingAudioStream *_audioStream; + uint16 _currSoundPriority; + + bool _needsUpdate; + Common::Array<Common::Rect> _dirtyRects; + Graphics::Surface _surface; + Common::List<Sprite> _sprites; + + uint _directoriesToStrip; + Common::ConfigFile _bookIni; + Common::List<Library> _libraries; + Common::Array<PendingPageChange> _pendingPageChanges; + + Common::Array<uint16> _stack; + Common::Array<uint16> _vars; + + Common::Array<QueuedScript> _queuedScripts; + Common::List<Animation *> _anims; + Common::List<Pipe *> _pipes; + + void onMouseDown(const Common::Point &pos); + void onMouseMove(const Common::Point &pos); + void onKeyDown(uint16 keyCode); + void setCursor(uint16 id, const Common::Point &offset); + void setCursorVisible(bool visible); + + bool _mouseEnabled; + bool _mouseVisible; + Common::Point _lastMousePos; + const Button *_lastButton; + uint16 _mouseSpriteId; + Common::Point _mouseOffset; + + Common::String getStringFromConfig(const Common::String §ion, const Common::String &key); + Common::String getFilename(const Common::String §ion, uint id); + void loadLibrary(uint id); + void unloadLibrary(uint id); + + bool hasResource(uint32 tag, uint16 id); + Common::SeekableReadStream *getResource(uint32 tag, uint16 id); + + void runEvent(uint16 id, int16 param1, int16 param2, int16 param3); + int16 runScript(uint16 id, int16 param1, int16 param2, int16 param3); + + int16 getArg(uint16 arg, uint16 type); + void setArg(uint16 arg, uint16 type, uint16 val); + void runScript(uint16 id); + int16 scriptFuncCall(uint16 id, int16 param1, int16 param2, int16 param3); + + void playAnimation(uint16 animId, int16 param1, int16 param2, int16 param3); + void stopAnimation(Animation *anim, bool localOnly = false, bool pipesOnly = false); + void playWaveForAnim(uint16 id, uint16 priority, bool bufferingOnly); + void processAnimFrame(); + + bool spriteVisible(uint16 id, uint16 animId); + Sprite *addSprite(uint16 id, uint16 animId, uint16 zorder, const Common::Point &pos); + void removeSprite(uint16 id, uint16 animId); + const Sprite *getSpriteAtPos(const Common::Point &pos); + const Button *getButtonFor(const Sprite *sprite, const Common::Point &pos); + + void dirtySprite(const Sprite &sprite); + void redraw(); + void loadCTBL(uint16 id, uint fadePercent); + void setBackground(uint16 id); + void decompressBitmap(uint16 type, Common::SeekableReadStream *stream, byte *buffer, uint32 size, uint width, uint height); + bool initSprite(Sprite &sprite); + Common::SeekableReadStream *getStreamForSprite(uint16 id); + void drawSprite(const Sprite &sprite); +}; + +} // End of namespace Composer + +#endif diff --git a/engines/composer/detection.cpp b/engines/composer/detection.cpp new file mode 100644 index 0000000000..cbd4dd1ebd --- /dev/null +++ b/engines/composer/detection.cpp @@ -0,0 +1,179 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "base/plugins.h" +#include "engines/advancedDetector.h" + +#include "composer/composer.h" + +namespace Composer { + +struct ComposerGameDescription { + ADGameDescription desc; + + int gameType; +}; + +int ComposerEngine::getGameType() const { + return _gameDescription->gameType; +} + +const char *ComposerEngine::getGameId() const { + return _gameDescription->desc.gameid; +} + +uint32 ComposerEngine::getFeatures() const { + return _gameDescription->desc.flags; +} + +Common::Language ComposerEngine::getLanguage() const { + return _gameDescription->desc.language; +} + +} + +static const PlainGameDescriptor composerGames[] = { + {"composer", "Composer Game"}, + {"darby", "Darby the Dragon"}, + {"gregory", "Gregory and the Hot Air Balloon"}, + {"liam", "Magic Tales: Liam Finds a Story"}, + {0, 0} +}; + +namespace Composer { + +using Common::GUIO_NONE; + +static const ComposerGameDescription gameDescriptions[] = { + // from Liam Finds a Story CD + { + { + "magictales", + "Magic Tales Demo: Baby Yaga, Samurai, Imo", + AD_ENTRY1("book.ini", "dbc98c566f4ac61b544443524585dccb"), + Common::EN_ANY, + Common::kPlatformWindows, + ADGF_USEEXTRAASTITLE, + Common::GUIO_NONE + }, + GType_ComposerV1 + }, + + { + { + "liam", + 0, + AD_ENTRY1("install.inf", "320d2f1d4f8dd96947676ae25d6688c6"), + Common::EN_ANY, + Common::kPlatformWindows, + ADGF_NO_FLAGS, + Common::GUIO_NONE + }, + GType_ComposerV2 + }, + + // from Liam Finds a Story CD + { + { + "magictales", + "Magic Tales Demo: Sleeping Cub, Princess & Crab", + AD_ENTRY1("book.ini", "3dede2522bb0886c95667b082987a87f"), + Common::EN_ANY, + Common::kPlatformWindows, + ADGF_USEEXTRAASTITLE, + Common::GUIO_NONE + }, + GType_ComposerV2 + }, + + { + { + "darby", + 0, + AD_ENTRY1("install.inf", "e83cc20ee18a2e138da1aadfc640dff2"), + Common::EN_ANY, + Common::kPlatformWindows, + ADGF_NO_FLAGS, + Common::GUIO_NONE + }, + GType_ComposerV2 + }, + + { + { + "gregory", + 0, + AD_ENTRY1("install.inf", "b7e9d6f7949d412dad0a183375069844"), + Common::EN_ANY, + Common::kPlatformWindows, + ADGF_NO_FLAGS, + Common::GUIO_NONE + }, + GType_ComposerV2 + }, + + { AD_TABLE_END_MARKER, 0 } +}; + +} // End of namespace Composer + +using namespace Composer; + +class ComposerMetaEngine : public AdvancedMetaEngine { +public: + ComposerMetaEngine() : AdvancedMetaEngine(Composer::gameDescriptions, sizeof(Composer::ComposerGameDescription), composerGames) { + _singleid = "composer"; + } + + virtual const char *getName() const { + return "Magic Composer Engine"; + } + + virtual const char *getOriginalCopyright() const { + return "Copyright (C) 1995-1999 Animation Magic"; + } + + virtual bool createInstance(OSystem *syst, Engine **engine, const ADGameDescription *desc) const; + virtual bool hasFeature(MetaEngineFeature f) const; +}; + +bool ComposerMetaEngine::createInstance(OSystem *syst, Engine **engine, const ADGameDescription *desc) const { + const Composer::ComposerGameDescription *gd = (const Composer::ComposerGameDescription *)desc; + if (gd) { + *engine = new Composer::ComposerEngine(syst, gd); + } + return gd != 0; +} + +bool ComposerMetaEngine::hasFeature(MetaEngineFeature f) const { + return false; +} + +bool Composer::ComposerEngine::hasFeature(EngineFeature f) const { + return (f == kSupportsRTL); +} + +#if PLUGIN_ENABLED_DYNAMIC(COMPOSER) +REGISTER_PLUGIN_DYNAMIC(COMPOSER, PLUGIN_TYPE_ENGINE, ComposerMetaEngine); +#else +REGISTER_PLUGIN_STATIC(COMPOSER, PLUGIN_TYPE_ENGINE, ComposerMetaEngine); +#endif diff --git a/engines/composer/graphics.cpp b/engines/composer/graphics.cpp new file mode 100644 index 0000000000..3de244970e --- /dev/null +++ b/engines/composer/graphics.cpp @@ -0,0 +1,758 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ +#include "common/scummsys.h" + +#include "graphics/palette.h" + +#include "audio/audiostream.h" +#include "audio/decoders/raw.h" + +#include "composer/composer.h" +#include "composer/graphics.h" +#include "composer/resource.h" + +namespace Composer { + +bool Sprite::contains(const Common::Point &pos) const { + Common::Point adjustedPos = pos - _pos; + + if (adjustedPos.x < 0 || adjustedPos.x >= _surface.w) + return false; + if (adjustedPos.y < 0 || adjustedPos.y >= _surface.h) + return false; + byte *pixels = (byte *)_surface.pixels; + return (pixels[(_surface.h - adjustedPos.y - 1) * _surface.w + adjustedPos.x] != 0); +} + +enum { + kAnimOpEvent = 1, + kAnimOpPlayWave = 2, + kAnimOpPlayAnim = 3, + kAnimOpDrawSprite = 4 +}; + +Animation::Animation(Common::SeekableReadStream *stream, uint16 id, Common::Point basePos, uint32 eventParam) + : _stream(stream), _id(id), _basePos(basePos), _eventParam(eventParam) { + uint32 size = _stream->readUint32LE(); + _state = _stream->readUint32LE() + 1; + + // probably total size? + uint32 unknown = _stream->readUint32LE(); + + debug(8, "anim: size %d, state %08x, unknown %08x", size, _state, unknown); + + for (uint i = 0; i < size; i++) { + AnimationEntry entry; + entry.op = _stream->readUint16LE(); + entry.priority = _stream->readUint16LE(); + entry.state = _stream->readUint16LE(); + entry.counter = 0; + entry.prevValue = 0; + debug(8, "anim entry: %04x, %04x, %04x", entry.op, entry.priority, entry.state); + _entries.push_back(entry); + } + + _offset = _stream->pos(); +} + +Animation::~Animation() { + delete _stream; +} + +void Animation::seekToCurrPos() { + _stream->seek(_offset, SEEK_SET); +} + +void ComposerEngine::playAnimation(uint16 animId, int16 x, int16 y, int16 eventParam) { + // First, we check if this animation is already playing, + // and if it is, we sabotage that running one first. + for (Common::List<Animation *>::iterator i = _anims.begin(); i != _anims.end(); i++) { + Animation *anim = *i; + if (anim->_id != animId) + continue; + + stopAnimation(*i); + } + + Common::SeekableReadStream *stream = NULL; + Pipe *newPipe = NULL; + + // First, check the existing pipes. + for (Common::List<Pipe *>::iterator j = _pipes.begin(); j != _pipes.end(); j++) { + Pipe *pipe = *j; + if (!pipe->hasResource(ID_ANIM, animId)) + continue; + stream = pipe->getResource(ID_ANIM, animId, false); + break; + } + + // If we didn't find it, try the libraries. + if (!stream) { + if (!hasResource(ID_ANIM, animId)) { + warning("ignoring attempt to play invalid anim %d", animId); + return; + } + stream = getResource(ID_ANIM, animId); + + uint32 type = 0; + for (Common::List<Library>::iterator i = _libraries.begin(); i != _libraries.end(); i++) + if (i->_archive->hasResource(ID_ANIM, animId)) { + type = i->_archive->getResourceFlags(ID_ANIM, animId); + break; + } + + // If the resource is a pipe itself, then load the pipe + // and then fish the requested animation out of it. + if (type != 1) { + newPipe = new Pipe(stream); + _pipes.push_front(newPipe); + stream = newPipe->getResource(ID_ANIM, animId, false); + } + } + + Animation *anim = new Animation(stream, animId, Common::Point(x, y), eventParam); + _anims.push_back(anim); + runEvent(kEventAnimStarted, animId, eventParam, 0); + if (newPipe) + newPipe->_anim = anim; +} + +void ComposerEngine::stopAnimation(Animation *anim, bool localOnly, bool pipesOnly) { + // disable the animation + anim->_state = 0; + + // stop any animations it may have spawned + for (uint j = 0; j < anim->_entries.size(); j++) { + AnimationEntry &entry = anim->_entries[j]; + if (!entry.prevValue) + continue; + if (localOnly) { + if (pipesOnly) + continue; + if (entry.op == kAnimOpDrawSprite) { + removeSprite(entry.prevValue, anim->_id); + } else if (entry.op == kAnimOpPlayWave) { + if (_currSoundPriority >= entry.priority) { + _mixer->stopAll(); + _audioStream = NULL; + } + } + } else { + if (entry.op != kAnimOpPlayAnim) + continue; + for (Common::List<Animation *>::iterator i = _anims.begin(); i != _anims.end(); i++) { + if ((*i)->_id == entry.prevValue) + stopAnimation(*i); + } + } + } + + // kill any pipe owned by the animation + for (Common::List<Pipe *>::iterator j = _pipes.begin(); j != _pipes.end(); j++) { + Pipe *pipe = *j; + if (pipe->_anim != anim) + continue; + j = _pipes.reverse_erase(j); + delete pipe; + break; + } +} + +void ComposerEngine::playWaveForAnim(uint16 id, uint16 priority, bool bufferingOnly) { + if (_audioStream && _audioStream->numQueuedStreams() != 0) { + if (_currSoundPriority < priority) + return; + if (_currSoundPriority > priority) { + _mixer->stopAll(); + _audioStream = NULL; + } + } + Common::SeekableReadStream *stream = NULL; + if (!bufferingOnly && hasResource(ID_WAVE, id)) { + stream = getResource(ID_WAVE, id); + } else { + for (Common::List<Pipe *>::iterator k = _pipes.begin(); k != _pipes.end(); k++) { + Pipe *pipe = *k; + if (!pipe->hasResource(ID_WAVE, id)) + continue; + stream = pipe->getResource(ID_WAVE, id, true); + break; + } + } + if (!stream) + return; + // FIXME: non-pipe buffers have fixed wav header (data at +44, size at +40) + byte *buffer = (byte *)malloc(stream->size()); + stream->read(buffer, stream->size()); + if (!_audioStream) + _audioStream = Audio::makeQueuingAudioStream(22050, false); + _audioStream->queueBuffer(buffer, stream->size(), DisposeAfterUse::YES, Audio::FLAG_UNSIGNED); + _currSoundPriority = priority; + delete stream; + if (!_mixer->isSoundHandleActive(_soundHandle)) + _mixer->playStream(Audio::Mixer::kSFXSoundType, &_soundHandle, _audioStream); +} + +void ComposerEngine::processAnimFrame() { + for (Common::List<Animation *>::iterator i = _anims.begin(); i != _anims.end(); i++) { + Animation *anim = *i; + + anim->seekToCurrPos(); + + if (anim->_state <= 1) { + bool normalEnd = (anim->_state == 1); + if (normalEnd) { + runEvent(kEventAnimDone, anim->_id, anim->_eventParam, 0); + } + stopAnimation(anim, true, normalEnd); + delete anim; + i = _anims.reverse_erase(i); + + continue; + } + + for (uint j = 0; j < anim->_entries.size(); j++) { + AnimationEntry &entry = anim->_entries[j]; + if (entry.op != kAnimOpEvent) + break; + if (entry.counter) { + entry.counter--; + } else { + if ((anim->_state > 1) && (anim->_stream->pos() + 2 > anim->_stream->size())) { + warning("anim with id %d ended too soon", anim->_id); + anim->_state = 0; + break; + } + + uint16 event = anim->_stream->readUint16LE(); + anim->_offset += 2; + if (event == 0xffff) { + entry.counter = anim->_stream->readUint16LE() - 1; + anim->_offset += 2; + } else { + debug(4, "anim: event %d", event); + runEvent(event, anim->_id, 0, 0); + } + } + } + } + + for (Common::List<Animation *>::iterator i = _anims.begin(); i != _anims.end(); i++) { + Animation *anim = *i; + + // did the anim get disabled? + if (anim->_state == 0) { + stopAnimation(anim, true, false); + delete anim; + i = _anims.reverse_erase(i); + continue; + } + + anim->_state--; + + bool foundWait = false; + for (uint j = 0; j < anim->_entries.size(); j++) { + AnimationEntry &entry = anim->_entries[j]; + + // only skip these at the start + if (!foundWait && (entry.op == kAnimOpEvent)) + continue; + foundWait = true; + + if (entry.counter) { + entry.counter--; + if ((entry.op == kAnimOpPlayWave) && entry.prevValue) { + debug(4, "anim: continue play wave %d", entry.prevValue); + playWaveForAnim(entry.prevValue, entry.priority, true); + } + } else { + anim->seekToCurrPos(); + if ((anim->_state > 1) && (anim->_stream->pos() + 2 > anim->_stream->size())) { + warning("anim with id %d ended too soon", anim->_id); + anim->_state = 0; + break; + } + + uint16 data = anim->_stream->readUint16LE(); + anim->_offset += 2; + if (data == 0xffff) { + entry.counter = anim->_stream->readUint16LE() - 1; + anim->_offset += 2; + } else { + switch (entry.op) { + case kAnimOpEvent: + debug(4, "anim: event %d", data); + runEvent(data, anim->_id, 0, 0); + break; + case kAnimOpPlayWave: + debug(4, "anim: play wave %d", data); + playWaveForAnim(data, entry.priority, false); + break; + case kAnimOpPlayAnim: + debug(4, "anim: play anim %d", data); + playAnimation(data, anim->_basePos.x, anim->_basePos.y, 1); + break; + case kAnimOpDrawSprite: + if (!data || (entry.prevValue && (data != entry.prevValue))) { + debug(4, "anim: erase sprite %d", entry.prevValue); + removeSprite(entry.prevValue, anim->_id); + } + if (data) { + int16 x = anim->_stream->readSint16LE(); + int16 y = anim->_stream->readSint16LE(); + Common::Point pos(x, y); + anim->_offset += 4; + uint16 animId = anim->_id; + if (anim->_state == entry.state) + animId = 0; + debug(4, "anim: draw sprite %d at (relative) %d,%d", data, x, y); + bool wasVisible = spriteVisible(data, animId); + addSprite(data, animId, entry.priority, anim->_basePos + pos); + if (wasVisible) { + // make sure modified sprite isn't removed by another entry + for (uint k = 0; k < anim->_entries.size(); k++) { + if (anim->_entries[k].op != kAnimOpDrawSprite) + continue; + if (anim->_entries[k].prevValue == data) + anim->_entries[k].prevValue = 1; + } + } + } + break; + default: + warning("unknown anim op %d", entry.op); + } + + entry.prevValue = data; + } + } + } + } + + for (Common::List<Pipe *>::iterator j = _pipes.begin(); j != _pipes.end(); j++) { + Pipe *pipe = *j; + pipe->nextFrame(); + } +} + +bool ComposerEngine::spriteVisible(uint16 id, uint16 animId) { + for (Common::List<Sprite>::iterator i = _sprites.begin(); i != _sprites.end(); i++) { + if (i->_id != id) + continue; + if (i->_animId && animId && (i->_animId != animId)) + continue; + return true; + } + + return false; +} + +Sprite *ComposerEngine::addSprite(uint16 id, uint16 animId, uint16 zorder, const Common::Point &pos) { + Sprite sprite; + bool foundSprite = false; + + // re-use old sprite, if any (the BMAP for this id might well have + // changed in the meantime, but the scripts depend on that!) + for (Common::List<Sprite>::iterator i = _sprites.begin(); i != _sprites.end(); i++) { + if (i->_id != id) + continue; + if (i->_animId && animId && (i->_animId != animId)) + continue; + + dirtySprite(*i); + + // if the zordering is identical, modify it in-place + if (i->_zorder == zorder) { + i->_animId = animId; + i->_pos = pos; + dirtySprite(*i); + return &(*i); + } + + // otherwise, take a copy and remove it from the list + sprite = *i; + foundSprite = true; + _sprites.erase(i); + break; + } + + sprite._animId = animId; + sprite._zorder = zorder; + sprite._pos = pos; + + if (!foundSprite) { + sprite._id = id; + if (!initSprite(sprite)) { + debug(1, "ignoring addSprite on invalid sprite %d", id); + return NULL; + } + } + + dirtySprite(sprite); + + for (Common::List<Sprite>::iterator i = _sprites.begin(); i != _sprites.end(); i++) { + if (sprite._zorder <= i->_zorder) + continue; + // insert *before* this sprite + _sprites.insert(i, sprite); + --i; + return &(*i); + } + _sprites.push_back(sprite); + return &_sprites.back(); +} + +void ComposerEngine::removeSprite(uint16 id, uint16 animId) { + for (Common::List<Sprite>::iterator i = _sprites.begin(); i != _sprites.end(); i++) { + if (!i->_id || (id && i->_id != id)) + continue; + if (i->_animId && animId && (i->_animId != animId)) + continue; + dirtySprite(*i); + i->_surface.free(); + i = _sprites.reverse_erase(i); + if (id) + break; + } +} + +const Sprite *ComposerEngine::getSpriteAtPos(const Common::Point &pos) { + for (Common::List<Sprite>::iterator i = _sprites.reverse_begin(); i != _sprites.end(); --i) { + // avoid highest-level objects (e.g. the cursor) + if (!i->_zorder) + continue; + + if (i->contains(pos)) + return &(*i); + } + + return NULL; +} + +void ComposerEngine::dirtySprite(const Sprite &sprite) { + Common::Rect rect(sprite._pos.x, sprite._pos.y, sprite._pos.x + sprite._surface.w, sprite._pos.y + sprite._surface.h); + rect.clip(_surface.w, _surface.h); + if (rect.isEmpty()) + return; + + for (uint i = 0; i < _dirtyRects.size(); i++) { + if (!_dirtyRects[i].intersects(rect)) + continue; + _dirtyRects[i].extend(rect); + return; + } + + _dirtyRects.push_back(rect); +} + +void ComposerEngine::redraw() { + if (!_needsUpdate && _dirtyRects.empty()) + return; + + for (Common::List<Sprite>::iterator i = _sprites.begin(); i != _sprites.end(); i++) { + Common::Rect rect(i->_pos.x, i->_pos.y, i->_pos.x + i->_surface.w, i->_pos.y + i->_surface.h); + bool intersects = false; + for (uint j = 0; j < _dirtyRects.size(); j++) { + if (!_dirtyRects[j].intersects(rect)) + continue; + intersects = true; + break; + } + if (!intersects) + continue; + drawSprite(*i); + } + + for (uint i = 0; i < _dirtyRects.size(); i++) { + const Common::Rect &rect = _dirtyRects[i]; + byte *pixels = (byte *)_surface.pixels + (rect.top * _surface.pitch) + rect.left; + _system->copyRectToScreen(pixels, _surface.pitch, rect.left, rect.top, rect.width(), rect.height()); + } + _system->updateScreen(); + + _needsUpdate = false; + _dirtyRects.clear(); +} + +void ComposerEngine::loadCTBL(uint16 id, uint fadePercent) { + Common::SeekableReadStream *stream = getResource(ID_CTBL, id); + + uint16 numEntries = stream->readUint16LE(); + debug(1, "CTBL: %d entries", numEntries); + + assert(numEntries <= 256); + assert(stream->size() == 2 + (numEntries * 3)); + + byte buffer[256 * 3]; + stream->read(buffer, numEntries * 3); + delete stream; + + for (uint i = 0; i < numEntries * 3; i++) + buffer[i] = ((unsigned int)buffer[i] * fadePercent) / 100; + + _system->getPaletteManager()->setPalette(buffer, 0, numEntries); + _needsUpdate = true; +} + +void ComposerEngine::setBackground(uint16 id) { + for (Common::List<Sprite>::iterator i = _sprites.begin(); i != _sprites.end(); i++) { + if (i->_id) + continue; + dirtySprite(*i); + i->_surface.free(); + i->_id = id; + if (!initSprite(*i)) + error("failed to set background %d", id); + dirtySprite(*i); + i->_id = 0; + return; + } + + Sprite *background = addSprite(id, 0, 0xffff, Common::Point()); + if (background) + background->_id = 0; +} + +static void decompressSLWM(byte *buffer, Common::SeekableReadStream *stream) { + uint bitsLeft = 0; + uint16 lastBits; + byte currBit; + while (true) { + if (bitsLeft == 0) { bitsLeft = 16; lastBits = stream->readUint16LE(); } + currBit = (lastBits & 1); lastBits >>= 1; bitsLeft--; + + if (currBit) { + // single byte + *buffer++ = stream->readByte(); + continue; + } + + if (bitsLeft == 0) { bitsLeft = 16; lastBits = stream->readUint16LE(); } + currBit = (lastBits & 1); lastBits >>= 1; bitsLeft--; + + uint start; + uint count; + if (currBit) { + uint orMask = stream->readByte(); + uint in = stream->readByte(); + count = in & 7; + start = ((in & ~7) << 5) | orMask; + if (!count) { + count = stream->readByte(); + if (!count) + break; + count -= 2; + } + } else { + // count encoded in the next two bits + count = 0; + + if (bitsLeft == 0) { bitsLeft = 16; lastBits = stream->readUint16LE(); } + currBit = (lastBits & 1); lastBits >>= 1; bitsLeft--; + + count = (count << 1) | currBit; + + if (bitsLeft == 0) { bitsLeft = 16; lastBits = stream->readUint16LE(); } + currBit = (lastBits & 1); lastBits >>= 1; bitsLeft--; + + count = (count << 1) | currBit; + + start = stream->readByte(); + } + + count += 2; + start++; + for (uint i = 0; i < count; i++) { + *buffer = *(buffer - start); + buffer++; + } + } +} + +// bitmap compression types +enum { + kBitmapUncompressed = 0, + kBitmapSpp32 = 1, + kBitmapSLW8 = 3, + kBitmapRLESLWM = 4, + kBitmapSLWM = 5 +}; + +void ComposerEngine::decompressBitmap(uint16 type, Common::SeekableReadStream *stream, byte *buffer, uint32 size, uint width, uint height) { + switch (type) { + case kBitmapUncompressed: + assert(stream->size() - (uint)stream->pos() == size); + assert(size == width * height); + stream->read(buffer, size); + break; + case kBitmapSpp32: + byte lookup[16]; + stream->read(lookup, 16); + while (size--) { + uint in = stream->readByte(); + byte lowBits = in & 0xF; + byte highBits = (in & 0xF0) >> 4; + if (highBits == 0xf) { + // run of a single color + uint count = (uint)stream->readByte() + 3; + size--; + memset(buffer, lookup[lowBits], count); + buffer += count; + } else { + // two pixels + *buffer++ = lookup[highBits]; + *buffer++ = lookup[lowBits]; + } + } + break; + case kBitmapSLW8: + while (size--) { + byte val = stream->readByte(); + if (val != 0xff) { + *buffer++ = val; + continue; + } + uint count = stream->readByte(); + size--; + + uint16 step; + if (!(count & 0x80)) { + step = stream->readByte(); + size--; + } else { + count = (count ^ 0x80); + step = stream->readUint16LE(); + size -= 2; + } + count += 4; + // this is often overlapping (for repeating patterns) + for (uint i = 0; i < count; i++) { + *buffer = *(buffer - step - 1); + buffer++; + } + } + break; + case kBitmapRLESLWM: + { + uint32 bufSize = stream->readUint32LE(); + byte *tempBuf = new byte[bufSize]; + decompressSLWM(tempBuf, stream); + + uint instrPos = tempBuf[0] + 1; + instrPos += READ_LE_UINT16(tempBuf + instrPos) + 2; + byte *instr = tempBuf + instrPos; + + uint line = 0; + while (line++ < height) { + uint pixels = 0; + + while (pixels < width) { + byte data = *instr++; + byte color = tempBuf[(data & 0x7F) + 1]; + if (!(data & 0x80)) { + *buffer++ = color; + pixels++; + } else { + byte count = *instr++; + if (!count) { + while (pixels++ < width) + *buffer++ = color; + break; + } + for (uint i = 0; i < count; i++) { + *buffer++ = color; + pixels++; + } + } + } + } + delete[] tempBuf; + } + break; + case kBitmapSLWM: + decompressSLWM(buffer, stream); + break; + default: + error("decompressBitmap can't handle type %d", type); + } +} + +Common::SeekableReadStream *ComposerEngine::getStreamForSprite(uint16 id) { + for (Common::List<Pipe *>::iterator k = _pipes.begin(); k != _pipes.end(); k++) { + Pipe *pipe = *k; + if (!pipe->hasResource(ID_BMAP, id)) + continue; + return pipe->getResource(ID_BMAP, id, true); + } + if (hasResource(ID_BMAP, id)) + return getResource(ID_BMAP, id); + return NULL; +} + +bool ComposerEngine::initSprite(Sprite &sprite) { + Common::SeekableReadStream *stream = getStreamForSprite(sprite._id); + if (!stream) + return false; + + uint16 type = stream->readUint16LE(); + int16 height = stream->readSint16LE(); + int16 width = stream->readSint16LE(); + uint32 size = stream->readUint32LE(); + debug(1, "loading BMAP: type %d, width %d, height %d, size %d", type, width, height, size); + + if (width > 0 && height > 0) { + sprite._surface.create(width, height, Graphics::PixelFormat::createFormatCLUT8()); + decompressBitmap(type, stream, (byte *)sprite._surface.pixels, size, width, height); + } else { + // there are some sprites (e.g. a -998x-998 one in Gregory's title screen) + // which have an invalid size, but the original engine doesn't notice for + // RLE sprites since the width/height is ignored until the actual draw + if (type != kBitmapRLESLWM) + error("sprite (type %d) had invalid size %dx%d", type, width, height); + delete stream; + return false; + } + delete stream; + + return true; +} + +void ComposerEngine::drawSprite(const Sprite &sprite) { + int x = sprite._pos.x; + int y = sprite._pos.y; + + // incoming data is BMP-style (bottom-up), so flip it + byte *pixels = (byte *)_surface.pixels; + for (int j = 0; j < sprite._surface.h; j++) { + if (j + y < 0) + continue; + if (j + y >= _surface.h) + break; + byte *in = (byte *)sprite._surface.pixels + (sprite._surface.h - j - 1) * sprite._surface.w; + byte *out = pixels + ((j + y) * _surface.w) + x; + for (int i = 0; i < sprite._surface.w; i++) + if ((x + i >= 0) && (x + i < _surface.w) && in[i]) + out[i] = in[i]; + } +} + +} // End of namespace Composer diff --git a/engines/composer/graphics.h b/engines/composer/graphics.h new file mode 100644 index 0000000000..e9d82920ba --- /dev/null +++ b/engines/composer/graphics.h @@ -0,0 +1,71 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef COMPOSER_GRAPHICS_H +#define COMPOSER_GRAPHICS_H + +#include "common/rect.h" +#include "graphics/surface.h" + +namespace Composer { + +class ComposerEngine; + +struct Sprite { + uint16 _id; + uint16 _animId; + uint16 _zorder; + Common::Point _pos; + Graphics::Surface _surface; + + bool contains(const Common::Point &pos) const; +}; + +struct AnimationEntry { + uint32 state; + uint16 op; + uint16 priority; + uint16 counter; + uint16 prevValue; +}; + +struct Animation { + Animation(Common::SeekableReadStream *stream, uint16 id, Common::Point basePos, uint32 eventParam); + ~Animation(); + + void seekToCurrPos(); + + uint16 _id; + Common::Point _basePos; + uint32 _eventParam; + + uint32 _state; + + Common::Array<AnimationEntry> _entries; + + uint32 _offset; + Common::SeekableReadStream *_stream; +}; + +} // End of namespace Composer + +#endif diff --git a/engines/composer/module.mk b/engines/composer/module.mk new file mode 100644 index 0000000000..72817de044 --- /dev/null +++ b/engines/composer/module.mk @@ -0,0 +1,16 @@ +MODULE := engines/composer + +MODULE_OBJS = \ + composer.o \ + detection.o \ + graphics.o \ + resource.o \ + scripting.o + +# This module can be built as a plugin +ifdef BUILD_PLUGINS +PLUGIN := 1 +endif + +# Include common rules +include $(srcdir)/rules.mk diff --git a/engines/composer/resource.cpp b/engines/composer/resource.cpp new file mode 100644 index 0000000000..e0893380bf --- /dev/null +++ b/engines/composer/resource.cpp @@ -0,0 +1,336 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "composer/resource.h" + +#include "common/debug.h" +#include "common/memstream.h" +#include "common/substream.h" +#include "common/util.h" +#include "common/textconsole.h" + +namespace Composer { + +// Base Archive code +// (copied from clone2727's mohawk code) + +Archive::Archive() { + _stream = 0; +} + +Archive::~Archive() { + close(); +} + +bool Archive::openFile(const Common::String &fileName) { + Common::File *file = new Common::File(); + + if (!file->open(fileName)) { + delete file; + return false; + } + + if (!openStream(file)) { + close(); + return false; + } + + return true; +} + +void Archive::close() { + _types.clear(); + delete _stream; _stream = 0; +} + +bool Archive::hasResource(uint32 tag, uint16 id) const { + if (!_types.contains(tag)) + return false; + + return _types[tag].contains(id); +} + +bool Archive::hasResource(uint32 tag, const Common::String &resName) const { + if (!_types.contains(tag) || resName.empty()) + return false; + + const ResourceMap &resMap = _types[tag]; + + for (ResourceMap::const_iterator it = resMap.begin(); it != resMap.end(); it++) + if (it->_value.name.matchString(resName)) + return true; + + return false; +} + +Common::SeekableReadStream *Archive::getResource(uint32 tag, uint16 id) { + if (!_types.contains(tag)) + error("Archive does not contain '%s' %04x", tag2str(tag), id); + + const ResourceMap &resMap = _types[tag]; + + if (!resMap.contains(id)) + error("Archive does not contain '%s' %04x", tag2str(tag), id); + + const Resource &res = resMap[id]; + + return new Common::SeekableSubReadStream(_stream, res.offset, res.offset + res.size); +} + +uint32 Archive::getResourceFlags(uint32 tag, uint16 id) const { + if (!_types.contains(tag)) + error("Archive does not contain '%s' %04x", tag2str(tag), id); + + const ResourceMap &resMap = _types[tag]; + + if (!resMap.contains(id)) + error("Archive does not contain '%s' %04x", tag2str(tag), id); + + const Resource &res = resMap[id]; + + return res.flags; +} + +uint32 Archive::getOffset(uint32 tag, uint16 id) const { + if (!_types.contains(tag)) + error("Archive does not contain '%s' %04x", tag2str(tag), id); + + const ResourceMap &resMap = _types[tag]; + + if (!resMap.contains(id)) + error("Archive does not contain '%s' %04x", tag2str(tag), id); + + return resMap[id].offset; +} + +uint16 Archive::findResourceID(uint32 tag, const Common::String &resName) const { + if (!_types.contains(tag) || resName.empty()) + return 0xFFFF; + + const ResourceMap &resMap = _types[tag]; + + for (ResourceMap::const_iterator it = resMap.begin(); it != resMap.end(); it++) + if (it->_value.name.matchString(resName)) + return it->_key; + + return 0xFFFF; +} + +Common::String Archive::getName(uint32 tag, uint16 id) const { + if (!_types.contains(tag)) + error("Archive does not contain '%s' %04x", tag2str(tag), id); + + const ResourceMap &resMap = _types[tag]; + + if (!resMap.contains(id)) + error("Archive does not contain '%s' %04x", tag2str(tag), id); + + return resMap[id].name; +} + +Common::Array<uint32> Archive::getResourceTypeList() const { + Common::Array<uint32> typeList; + + for (TypeMap::const_iterator it = _types.begin(); it != _types.end(); it++) + typeList.push_back(it->_key); + + return typeList; +} + +Common::Array<uint16> Archive::getResourceIDList(uint32 type) const { + Common::Array<uint16> idList; + + if (!_types.contains(type)) + return idList; + + const ResourceMap &resMap = _types[type]; + + for (ResourceMap::const_iterator it = resMap.begin(); it != resMap.end(); it++) + idList.push_back(it->_key); + + return idList; +} + +// Composer Archive code + +bool ComposerArchive::openStream(Common::SeekableReadStream *stream) { + // Make sure no other file is open... + close(); + + bool newStyle = false; + uint32 headerSize = stream->readUint32LE(); + if (headerSize == SWAP_CONSTANT_32(ID_LBRC)) { + // new-style file + newStyle = true; + headerSize = stream->readUint32LE(); + uint32 zeros = stream->readUint32LE(); + if (zeros != 0) + error("invalid LBRC header (%d instead of zeros)", zeros); + } + + uint16 numResourceTypes = stream->readUint16LE(); + if (newStyle) { + uint16 unknown = stream->readUint16LE(); + debug(4, "skipping unknown %04x", unknown); + } + + debug(4, "Reading LBRC resource table with %d entries", numResourceTypes); + for (uint i = 0; i < numResourceTypes; i++) { + uint32 tag = stream->readUint32BE(); + uint32 tableOffset = stream->readUint32LE(); + debug(4, "Type '%s' at offset %d", tag2str(tag), tableOffset); + // starting from the start of the resource table, which differs + // according to whether we have the 10 extra bytes for newStyle + if (newStyle) + tableOffset += 16; + else + tableOffset += 6; + + ResourceMap &resMap = _types[tag]; + + uint32 oldPos = stream->pos(); + stream->seek(tableOffset); + + while (true) { + if (stream->eos()) + error("LBRC file ran out of stream"); + + uint32 offset, size, id, flags; + if (newStyle) { + offset = stream->readUint32LE(); + if (!offset) + break; + size = stream->readUint32LE(); + id = stream->readUint16LE(); + flags = stream->readUint16LE(); // set to 1 for preload, otherwise no preload + /*uint32 junk = */ stream->readUint32LE(); + } else { + id = stream->readUint16LE(); + if (!id) + break; + offset = stream->readUint32LE(); + offset += headerSize; + size = stream->readUint32LE(); + flags = stream->readUint16LE(); // FIXME + + } + + Resource &res = resMap[id]; + res.offset = offset; + res.size = size; + res.flags = flags; + debug(4, "Id %d, offset %d, size %d, flags %08x", id, offset, size, flags); + } + + stream->seek(oldPos); + } + + _stream = stream; + return true; +} + +Pipe::Pipe(Common::SeekableReadStream *stream) { + _offset = 0; + _stream = stream; + + nextFrame(); +} + +void Pipe::nextFrame() { + if (_offset == (uint)_stream->size()) + return; + + _stream->seek(_offset, SEEK_SET); + + uint32 tagCount = _stream->readUint32LE(); + _offset += 4; + for (uint i = 0; i < tagCount; i++) { + uint32 tag = _stream->readUint32BE(); + uint32 count = _stream->readUint32LE(); + _offset += 8; + + ResourceMap &resMap = _types[tag]; + + _offset += (12 * count); + for (uint j = 0; j < count; j++) { + uint32 offset = _stream->readUint32LE(); + uint32 size = _stream->readUint32LE(); + uint16 id = _stream->readUint16LE(); + uint32 unknown = _stream->readUint16LE(); // frame id? + debug(9, "pipe: %s/%d: offset %d, size %d, unknown %d", tag2str(tag), id, offset, size, unknown); + + PipeResourceEntry entry; + entry.size = size; + entry.offset = _offset; + resMap[id].entries.push_back(entry); + + _offset += size; + } + _stream->seek(_offset, SEEK_SET); + } +} + +bool Pipe::hasResource(uint32 tag, uint16 id) const { + if (!_types.contains(tag)) + return false; + + return _types[tag].contains(id); +} + +Common::SeekableReadStream *Pipe::getResource(uint32 tag, uint16 id, bool buffering) { + if (!_types.contains(tag)) + error("Pipe does not contain '%s' %04x", tag2str(tag), id); + + const ResourceMap &resMap = _types[tag]; + + if (!resMap.contains(id)) + error("Archive does not contain '%s' %04x", tag2str(tag), id); + + const PipeResource &res = resMap[id]; + + if (res.entries.size() == 1) { + Common::SeekableReadStream *stream = new Common::SeekableSubReadStream(_stream, + res.entries[0].offset, res.entries[0].offset + res.entries[0].size); + if (buffering) + _types[tag].erase(id); + return stream; + } + + // If there are multiple entries in the pipe, we have to concaternate them together. + + uint32 size = 0; + for (uint i = 0; i < res.entries.size(); i++) + size += res.entries[i].size; + + byte *buffer = (byte *)malloc(size); + uint32 offset = 0; + for (uint i = 0; i < res.entries.size(); i++) { + _stream->seek(res.entries[i].offset, SEEK_SET); + _stream->read(buffer + offset, res.entries[i].size); + offset += res.entries[i].size; + } + if (buffering) + _types[tag].erase(id); + return new Common::MemoryReadStream(buffer, size, DisposeAfterUse::YES); +} + +} // End of namespace Composer diff --git a/engines/composer/resource.h b/engines/composer/resource.h new file mode 100644 index 0000000000..f97a628933 --- /dev/null +++ b/engines/composer/resource.h @@ -0,0 +1,124 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "common/scummsys.h" +#include "common/endian.h" +#include "common/hashmap.h" +#include "common/file.h" +#include "common/str.h" + +#ifndef COMPOSER_RESOURCE_H +#define COMPOSER_RESOURCE_H + +namespace Composer { + +class Animation; + +#define ID_LBRC MKTAG('L','B','R','C') // Main FourCC + +#define ID_ANIM MKTAG('A','N','I','M') // Animation +#define ID_BMAP MKTAG('B','M','A','P') // Bitmap +#define ID_BUTN MKTAG('B','U','T','N') // Button +#define ID_CTBL MKTAG('C','T','B','L') // Color Table +#define ID_EVNT MKTAG('E','V','N','T') // Event +#define ID_PIPE MKTAG('P','I','P','E') // Pipe +#define ID_SCRP MKTAG('S','C','R','P') // Script +#define ID_VARI MKTAG('V','A','R','I') // Variables +#define ID_WAVE MKTAG('W','A','V','E') // Wave + +class Archive { +public: + Archive(); + virtual ~Archive(); + + bool openFile(const Common::String &fileName); + virtual bool openStream(Common::SeekableReadStream *stream) = 0; + void close(); + + bool isOpen() const { return _stream != 0; } + + bool hasResource(uint32 tag, uint16 id) const; + bool hasResource(uint32 tag, const Common::String &resName) const; + Common::SeekableReadStream *getResource(uint32 tag, uint16 id); + uint32 getResourceFlags(uint32 tag, uint16 id) const; + uint32 getOffset(uint32 tag, uint16 id) const; + uint16 findResourceID(uint32 tag, const Common::String &resName) const; + Common::String getName(uint32 tag, uint16 id) const; + + Common::Array<uint32> getResourceTypeList() const; + Common::Array<uint16> getResourceIDList(uint32 type) const; + +protected: + Common::SeekableReadStream *_stream; + + struct Resource { + uint32 offset; + uint32 size; + Common::String name; + uint32 flags; + }; + + typedef Common::HashMap<uint16, Resource> ResourceMap; + typedef Common::HashMap<uint32, ResourceMap> TypeMap; + TypeMap _types; +}; + +class ComposerArchive : public Archive { +public: + ComposerArchive() : Archive() {} + ~ComposerArchive() {} + + bool openStream(Common::SeekableReadStream *stream); +}; + +struct PipeResourceEntry { + uint32 size; + uint32 offset; +}; + +struct PipeResource { + Common::Array<PipeResourceEntry> entries; +}; + +class Pipe { +public: + Pipe(Common::SeekableReadStream *stream); + void nextFrame(); + + Animation *_anim; + + bool hasResource(uint32 tag, uint16 id) const; + Common::SeekableReadStream *getResource(uint32 tag, uint16 id, bool buffering); + +protected: + Common::SeekableReadStream *_stream; + + typedef Common::HashMap<uint16, PipeResource> ResourceMap; + typedef Common::HashMap<uint32, ResourceMap> TypeMap; + TypeMap _types; + + uint32 _offset; +}; + +} // End of namespace Composer + +#endif diff --git a/engines/composer/scripting.cpp b/engines/composer/scripting.cpp new file mode 100644 index 0000000000..470780583a --- /dev/null +++ b/engines/composer/scripting.cpp @@ -0,0 +1,733 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ +#include "common/scummsys.h" +#include "common/savefile.h" + +#include "composer/composer.h" +#include "composer/graphics.h" +#include "composer/resource.h" + +namespace Composer { + +// new script ops +enum { + kOpPlusPlus = 0x1, + kOpMinusMinus = 0x2, + kOpAssign = 0x3, + kOpAdd = 0x4, + kOpSubtract = 0x5, + kOpMultiply = 0x6, + kOpDivide = 0x7, + kOpModulo = 0x8, + kOpMaybeAlsoAssign = 0x9, + kOpBooleanAssign = 0xA, + kOpNegate = 0xB, + kOpAnd = 0xC, + kOpOr = 0xD, + kOpXor = 0xE, + kOpNot = 0xF, + kOpSqrt = 0x10, + kOpRandom = 0x11, + kOpExecuteScript = 0x12, + kOpCallFunc = 0x13, + kOpBoolLessThanEq = 0x17, + kOpBoolLessThan = 0x16, + kOpBoolGreaterThanEq = 0x15, + kOpBoolGreaterThan = 0x14, + kOpBoolEqual = 0x18, + kOpBoolNotEqual = 0x19, + kOpSaveArgs = 0x1A, + kOpRestoreArgs = 0x1B, + kOpReturn = 0x20, + kOpLessThanEq = 0x22, + kOpLessThan = 0x21, + kOpGreaterThanEq = 0x24, + kOpGreaterThan = 0x23, + kOpEqual = 0x25, + kOpNotEqual = 0x26, + kOpJump = 0x80, + kOpJumpIfNot = 0x81, + kOpJumpIf = 0x82, + kOpJumpIfNotValue = 0x83, + kOpJumpIfValue = 0x84 +}; + +enum { + kFuncPlayAnim = 35001, + kFuncStopAnim = 35002, + // (no 35003) + kFuncQueueScript = 35004, + kFuncDequeueScript = 35005, + kFuncSetCursor = 35006, + kFuncGetCursor = 35007, + kFuncShowCursor = 35008, + kFuncHideCursor = 35009, + // (no 35010) + kFuncActivateButton = 35011, + kFuncDeactivateButton = 35012, + kFuncNewPage = 35013, + kFuncLoadPage = 35014, + kFuncUnloadPage = 35015, + kFuncSetPalette = 35016, + kFuncSaveVars = 35017, + kFuncLoadVars = 35018, + kFuncQueueScriptOnce = 35019, + kFuncGetMousePos = 35020, + kFuncChangeBackground = 35021, + kFuncSetBackgroundColor = 35022, + kFuncClearSprites = 35023, + kFuncAddSprite = 35024, + kFuncRemoveSprite = 35025, + kFuncQuit = 35026, + kFuncSaveData = 35027, + kFuncLoadData = 35028, + kFuncGetSpriteSize = 35029 +}; + +void ComposerEngine::runEvent(uint16 id, int16 param1, int16 param2, int16 param3) { + if (!hasResource(ID_EVNT, id)) + return; + + Common::SeekableReadStream *stream = getResource(ID_EVNT, id); + if (stream->size() != 2) + error("bad EVNT size %d", stream->size()); + uint16 scriptId = stream->readUint16LE(); + delete stream; + + if (!scriptId) + return; + + debug(2, "running event %d via script %d(%d, %d, %d)", id, scriptId, param1, param2, param3); + + runScript(scriptId, param1, param2, param3); +} + +int16 ComposerEngine::runScript(uint16 id, int16 param1, int16 param2, int16 param3) { + _vars[1] = param1; + _vars[2] = param2; + _vars[3] = param3; + + runScript(id); + + return _vars[0]; +} + +int16 ComposerEngine::getArg(uint16 arg, uint16 type) { + switch (type) { + case 0: + return (int16)arg; + case 1: + return (int16)_vars[arg]; + case 2: + return (int16)_vars[_vars[arg]]; + default: + error("invalid argument type %d (getting arg %d)", type, arg); + } +} + +void ComposerEngine::setArg(uint16 arg, uint16 type, uint16 val) { + switch (type) { + case 1: + _vars[arg] = val; + break; + case 2: + _vars[_vars[arg]] = val; + break; + default: + error("invalid argument type %d (setting arg %d)", type, arg); + } + +} + +void ComposerEngine::runScript(uint16 id) { + if (!hasResource(ID_SCRP, id)) { + warning("ignoring attempt to run script %d, because it doesn't exist", id); + return; + } + + uint stackBase = _stack.size(); + _stack.resize(_stack.size() + 19); + + Common::SeekableReadStream *stream = getResource(ID_SCRP, id); + if (stream->size() < 2) + error("SCRP was too small (%d)", stream->size()); + uint16 size = stream->readUint16LE(); + if (stream->size() < 2 + 2*size) + error("SCRP was too small (%d, but claimed %d entries)", stream->size(), size); + uint16 *script = new uint16[size]; + for (uint i = 0; i < size; i++) + script[i] = stream->readUint16LE(); + delete stream; + + uint16 pos = 0; + bool lastResult = false; + while (pos < size) { + int16 val1, val2, val3; + + byte op = (byte)script[pos]; + uint numParams = (script[pos] & 0x300) >> 8; // 2 bits + if (pos + numParams >= size) + error("script ran out of content"); + uint arg1 = (script[pos] & 0xc00) >> 10; // 2 bits + uint arg2 = (script[pos] & 0x3000) >> 12; // 2 bits + uint arg3 = (script[pos] & 0xC000) >> 14; // 2 bits + switch (op) { + case kOpPlusPlus: + if (numParams != 1) + error("kOpPlusPlus had wrong number of params (%d)", numParams); + val1 = getArg(script[pos + 1], arg1); + debug(9, "[%d/%d]++ (now %d)", script[pos + 1], arg1, val1 + 1); + setArg(script[pos + 1], arg1, val1 + 1); + break; + case kOpMinusMinus: + if (numParams != 1) + error("kOpMinusMinus had wrong number of params (%d)", numParams); + val1 = getArg(script[pos + 1], arg1); + debug(9, "[%d/%d]-- (now %d)", script[pos + 1], arg1, val1 - 1); + setArg(script[pos + 1], arg1, val1 - 1); + break; + case kOpAssign: + if (numParams != 2) + error("kOpAssign had wrong number of params (%d)", numParams); + val2 = getArg(script[pos + 2], arg2); + debug(9, "[%d/%d] = [%d/%d] (%d)", script[pos + 1], arg1, script[pos + 2], arg2, val2); + setArg(script[pos + 1], arg1, val2); + break; + case kOpAdd: + if (numParams != 3) + error("kOpAdd had wrong number of params (%d)", numParams); + val2 = getArg(script[pos + 2], arg2); + val3 = getArg(script[pos + 3], arg3); + debug(9, "[%d/%d] = [%d/%d]=%d + [%d/%d]=%d (%d)", script[pos + 1], arg1, script[pos + 2], arg2, val2, script[pos+3], arg3, val3, val2 + val3); + setArg(script[pos + 1], arg1, val2 + val3); + break; + case kOpSubtract: + if (numParams != 3) + error("kOpSubtract had wrong number of params (%d)", numParams); + val2 = getArg(script[pos + 2], arg2); + val3 = getArg(script[pos + 3], arg3); + debug(9, "[%d/%d] = [%d/%d]=%d - [%d/%d]=%d (%d)", script[pos + 1], arg1, script[pos + 2], arg2, val2, script[pos+3], arg3, val3, val2 - val3); + setArg(script[pos + 1], arg1, val2 - val3); + break; + case kOpMultiply: + if (numParams != 3) + error("kOpMultiply had wrong number of params (%d)", numParams); + val2 = getArg(script[pos + 2], arg2); + val3 = getArg(script[pos + 3], arg3); + debug(9, "[%d/%d] = [%d/%d]=%d * [%d/%d]=%d (%d)", script[pos + 1], arg1, script[pos + 2], arg2, val2, script[pos+3], arg3, val3, val2 * val3); + setArg(script[pos + 1], arg1, val2 * val3); + break; + case kOpDivide: + if (numParams != 3) + error("kOpDivide had wrong number of params (%d)", numParams); + val2 = getArg(script[pos + 2], arg2); + val3 = getArg(script[pos + 3], arg3); + if (val3 == 0) + error("script tried to divide by zero"); + debug(9, "[%d/%d] = [%d/%d]=%d / [%d/%d]=%d (%d)", script[pos + 1], arg1, script[pos + 2], arg2, val2, script[pos+3], arg3, val3, val2 / val3); + setArg(script[pos + 1], arg1, val2 / val3); + break; + case kOpModulo: + if (numParams != 3) + error("kOpModulo had wrong number of params (%d)", numParams); + val2 = getArg(script[pos + 2], arg2); + val3 = getArg(script[pos + 3], arg3); + if (val3 == 0) + error("script tried to divide by zero (modulo)"); + debug(9, "[%d/%d] = [%d/%d]=%d %% [%d/%d]=%d (%d)", script[pos + 1], arg1, script[pos + 2], arg2, val2, script[pos+3], arg3, val3, val2 % val3); + setArg(script[pos + 1], arg1, val2 % val3); + break; + case kOpMaybeAlsoAssign: + if (numParams != 2) + error("kOpMaybeAlsoAssign had wrong number of params (%d)", numParams); + val2 = getArg(script[pos + 2], arg2); + debug(9, "[%d/%d] =(?) [%d/%d] (%d)", script[pos + 1], arg1, script[pos + 2], arg2, val2); + setArg(script[pos + 1], arg1, val2); + break; + case kOpBooleanAssign: + if (numParams != 2) + error("kOpBooleanAssign had wrong number of params (%d)", numParams); + val2 = getArg(script[pos + 2], arg2); + debug(9, "[%d/%d] = [%d/%d] (%d) ? 1 : 0", script[pos + 1], arg1, script[pos + 2], arg2, val2); + setArg(script[pos + 1], arg1, val2 ? 1 : 0); + break; + case kOpNegate: + if (numParams != 2) + error("kOpNegate had wrong number of params (%d)", numParams); + val2 = getArg(script[pos + 2], arg2); + debug(9, "[%d/%d] = -[%d/%d] (%d)", script[pos + 1], arg1, script[pos + 2], arg2, val2); + setArg(script[pos + 1], arg1, -val2); + break; + case kOpAnd: + if (numParams != 3) + error("kOpAnd had wrong number of params (%d)", numParams); + val2 = getArg(script[pos + 2], arg2); + val3 = getArg(script[pos + 3], arg3); + debug(9, "[%d/%d] = [%d/%d]=%d & [%d/%d]=%d (%d)", script[pos + 1], arg1, script[pos + 2], arg2, val2, script[pos+3], arg3, val3, val2 & val3); + setArg(script[pos + 1], arg1, val2 & val3); + break; + case kOpOr: + if (numParams != 3) + error("kOpOr had wrong number of params (%d)", numParams); + val2 = getArg(script[pos + 2], arg2); + val3 = getArg(script[pos + 3], arg3); + debug(9, "[%d/%d] = [%d/%d]=%d | [%d/%d]=%d (%d)", script[pos + 1], arg1, script[pos + 2], arg2, val2, script[pos+3], arg3, val3, val2 | val3); + setArg(script[pos + 1], arg1, val2 | val3); + break; + case kOpXor: + if (numParams != 3) + error("kOpXor had wrong number of params (%d)", numParams); + val2 = getArg(script[pos + 2], arg2); + val3 = getArg(script[pos + 3], arg3); + debug(9, "[%d/%d] = [%d/%d]=%d ^ [%d/%d]=%d (%d)", script[pos + 1], arg1, script[pos + 2], arg2, val2, script[pos+3], arg3, val3, val2 ^ val3); + setArg(script[pos + 1], arg1, val2 ^ val3); + break; + case kOpNot: + if (numParams != 2) + error("kOpNot had wrong number of params (%d)", numParams); + val2 = getArg(script[pos + 2], arg2); + debug(9, "[%d/%d] = ![%d/%d] (!%d)", script[pos + 1], arg1, script[pos + 2], arg2, val2); + setArg(script[pos + 1], arg1, val2 ? 0 : 1); + break; + case kOpSqrt: + if (numParams != 2) + error("kOpSqrt had wrong number of params (%d)", numParams); + val2 = getArg(script[pos + 2], arg2); + debug(9, "[%d/%d] = sqrt([%d/%d] (%d))", script[pos + 1], arg1, script[pos + 2], arg2, val2); + setArg(script[pos + 1], arg1, (int16)sqrt((double)val2)); + break; + case kOpRandom: + if (numParams != 3) + error("kOpRandom had wrong number of params (%d)", numParams); + val2 = getArg(script[pos + 2], arg2); + val3 = getArg(script[pos + 3], arg3); + val1 = _rnd->getRandomNumberRng(val2, val3); + debug(9, "[%d/%d] = rnd([%d/%d]=%d, [%d/%d]=%d) (%d)", script[pos + 1], arg1, script[pos + 2], arg2, val2, script[pos+3], arg3, val3, val1); + setArg(script[pos + 1], arg1, val1); + break; + case kOpExecuteScript: + if (numParams != 1) + error("kOpExecuteScript had wrong number of params (%d)", numParams); + val1 = getArg(script[pos + 1], arg1); + debug(8, "run script [%d/%d]=%d", script[pos + 1], arg1, val1); + runScript(val1); + debug(8, "done run script"); + break; + case kOpCallFunc: + if (numParams != 1) + error("kOpCallFunc had wrong number of params (%d)", numParams); + val1 = getArg(script[pos + 1], arg1); + debug(8, "%d(%d, %d, %d)", (uint16)val1, _vars[1], _vars[2], _vars[3]); + _vars[0] = scriptFuncCall(val1, _vars[1], _vars[2], _vars[3]); + break; + case kOpBoolLessThanEq: + if (numParams != 2) + error("kOpBoolLessThanEq had wrong number of params (%d)", numParams); + val1 = getArg(script[pos + 1], arg1); + val2 = getArg(script[pos + 2], arg2); + debug(9, "[%d/%d] <= [%d/%d]? (%d <= %d)", script[pos + 1], arg1, script[pos + 2], arg2, val1, val2); + lastResult = (val1 <= val2); + break; + case kOpBoolLessThan: + if (numParams != 2) + error("kOpBoolLessThan had wrong number of params (%d)", numParams); + val1 = getArg(script[pos + 1], arg1); + val2 = getArg(script[pos + 2], arg2); + debug(9, "[%d/%d] < [%d/%d]? (%d < %d)", script[pos + 1], arg1, script[pos + 2], arg2, val1, val2); + lastResult = (val1 < val2); + break; + case kOpBoolGreaterThanEq: + if (numParams != 2) + error("kOpBoolGreaterThanEq had wrong number of params (%d)", numParams); + val1 = getArg(script[pos + 1], arg1); + val2 = getArg(script[pos + 2], arg2); + debug(9, "[%d/%d] >= [%d/%d]? (%d >= %d)", script[pos + 1], arg1, script[pos + 2], arg2, val1, val2); + lastResult = (val1 >= val2); + break; + case kOpBoolGreaterThan: + if (numParams != 2) + error("kOpBoolGreaterThan had wrong number of params (%d)", numParams); + val1 = getArg(script[pos + 1], arg1); + val2 = getArg(script[pos + 2], arg2); + debug(9, "[%d/%d] > [%d/%d]? (%d > %d)", script[pos + 1], arg1, script[pos + 2], arg2, val1, val2); + lastResult = (val1 > val2); + break; + case kOpBoolEqual: + if (numParams != 2) + error("kOpBoolEqual had wrong number of params (%d)", numParams); + val1 = getArg(script[pos + 1], arg1); + val2 = getArg(script[pos + 2], arg2); + debug(9, "[%d/%d] == [%d/%d]? (%d == %d)", script[pos + 1], arg1, script[pos + 2], arg2, val1, val2); + lastResult = (val1 == val2); + break; + case kOpBoolNotEqual: + if (numParams != 2) + error("kOpBoolNotEqual had wrong number of params (%d)", numParams); + val1 = getArg(script[pos + 1], arg1); + val2 = getArg(script[pos + 2], arg2); + debug(9, "[%d/%d] != [%d/%d]? (%d != %d)", script[pos + 1], arg1, script[pos + 2], arg2, val1, val2); + lastResult = (val1 != val2); + break; + case kOpSaveArgs: + if (numParams != 0) + error("kOpSaveArgs had wrong number of params (%d)", numParams); + debug(9, "save args"); + for (uint i = 1; i < 19; i++) + _stack[stackBase + i] = _vars[i]; + break; + case kOpRestoreArgs: + if (numParams != 0) + error("kOpRestoreArgs had wrong number of params (%d)", numParams); + debug(9, "restore args"); + for (uint i = 1; i < 19; i++) + _vars[i] = _stack[stackBase + i]; + break; + case kOpReturn: + if (numParams != 1) + error("kOpReturn had wrong number of params (%d)", numParams); + val1 = getArg(script[pos + 1], arg1); + debug(9, "return [%d/%d]=%d", script[pos + 1], arg1, val1); + _vars[0] = val1; + break; + case kOpLessThanEq: + if (numParams != 3) + error("kOpLessThanEq had wrong number of params (%d)", numParams); + val2 = getArg(script[pos + 2], arg2); + val3 = getArg(script[pos + 3], arg3); + debug(9, "[%d/%d] = [%d/%d] <= [%d/%d]? (%d <= %d)", script[pos + 1], arg1, script[pos + 2], arg2, script[pos + 3], arg3, val3, val2); + setArg(script[pos + 1], arg1, (val3 <= val2) ? 1 : 0); + break; + case kOpLessThan: + if (numParams != 3) + error("kOpLessThan had wrong number of params (%d)", numParams); + val2 = getArg(script[pos + 2], arg2); + val3 = getArg(script[pos + 3], arg3); + debug(9, "[%d/%d] = [%d/%d] < [%d/%d]? (%d < %d)", script[pos + 1], arg1, script[pos + 2], arg2, script[pos + 3], arg3, val3, val2); + setArg(script[pos + 1], arg1, (val3 < val2) ? 1 : 0); + break; + case kOpGreaterThanEq: + if (numParams != 3) + error("kOpGreaterThanEq had wrong number of params (%d)", numParams); + val2 = getArg(script[pos + 2], arg2); + val3 = getArg(script[pos + 3], arg3); + debug(9, "[%d/%d] = [%d/%d] >= [%d/%d]? (%d >= %d)", script[pos + 1], arg1, script[pos + 2], arg2, script[pos + 3], arg3, val3, val2); + setArg(script[pos + 1], arg1, (val3 >= val2) ? 1 : 0); + break; + case kOpGreaterThan: + if (numParams != 3) + error("kOpGreaterThan had wrong number of params (%d)", numParams); + val2 = getArg(script[pos + 2], arg2); + val3 = getArg(script[pos + 3], arg3); + debug(9, "[%d/%d] = [%d/%d] > [%d/%d]? (%d > %d)", script[pos + 1], arg1, script[pos + 2], arg2, script[pos + 3], arg3, val3, val2); + setArg(script[pos + 1], arg1, (val3 > val2) ? 1 : 0); + break; + case kOpEqual: + if (numParams != 3) + error("kOpEqual had wrong number of params (%d)", numParams); + val2 = getArg(script[pos + 2], arg2); + val3 = getArg(script[pos + 3], arg3); + debug(9, "[%d/%d] = [%d/%d] == [%d/%d]? (%d == %d)", script[pos + 1], arg1, script[pos + 2], arg2, script[pos + 3], arg3, val2, val3); + setArg(script[pos + 1], arg1, (val3 == val2) ? 1 : 0); + break; + case kOpNotEqual: + if (numParams != 3) + error("kOpNotEqual had wrong number of params (%d)", numParams); + val2 = getArg(script[pos + 2], arg2); + val3 = getArg(script[pos + 3], arg3); + debug(9, "[%d/%d] = [%d/%d] != [%d/%d]? (%d != %d)", script[pos + 1], arg1, script[pos + 2], arg2, script[pos + 3], arg3, val2, val3); + setArg(script[pos + 1], arg1, (val3 != val2) ? 1 : 0); + break; + case kOpJump: + if (numParams != 1) + error("kOpJump had wrong number of params (%d)", numParams); + val1 = getArg(script[pos + 1], arg1); + debug(9, "jump by [%d/%d]=%d", script[pos + 1], arg1, val1); + pos += val1; + break; + case kOpJumpIfNot: + if (numParams != 1) + error("kOpJumpIfNot had wrong number of params (%d)", numParams); + if (lastResult) + break; + val1 = getArg(script[pos + 1], arg1); + debug(9, "jump if not, by [%d/%d]=%d", script[pos + 1], arg1, val1); + pos += val1; + break; + case kOpJumpIf: + if (numParams != 1) + error("kOpJumpIf had wrong number of params (%d)", numParams); + if (!lastResult) + break; + val1 = getArg(script[pos + 1], arg1); + debug(9, "jump if, by [%d/%d]=%d", script[pos + 1], arg1, val1); + pos += val1; + break; + case kOpJumpIfNotValue: + if (numParams != 2) + error("kOpJumpIfNotValue had wrong number of params (%d)", numParams); + val1 = getArg(script[pos + 1], arg1); + val2 = getArg(script[pos + 2], arg2); + debug(9, "jump if not [%d/%d]=%d", script[pos + 1], arg1, val1); + if (val1) + break; + debug(9, "--> jump by [%d/%d]=%d", script[pos + 2], arg2, val2); + pos += val2; + break; + case kOpJumpIfValue: + if (numParams != 2) + error("kOpJumpIfValue had wrong number of params (%d)", numParams); + val1 = getArg(script[pos + 1], arg1); + val2 = getArg(script[pos + 2], arg2); + debug(9, "jump if [%d/%d]=%d", script[pos + 1], arg1, val1); + if (!val1) + break; + debug(9, "--> jump by [%d/%d]=%d", script[pos + 2], arg2, val2); + pos += val2; + break; + default: + error("unknown script op 0x%02x", op); + } + pos += (1 + numParams); + + if (op == kOpReturn) + break; + } + + delete[] script; + _stack.resize(_stack.size() - 19); +} + +int16 ComposerEngine::scriptFuncCall(uint16 id, int16 param1, int16 param2, int16 param3) { + switch (id) { + case kFuncPlayAnim: + debug(3, "kFuncPlayAnim(%d, %d, %d)", param1, param2, param3); + playAnimation(param1, param2, param3, 0); + return 1; // TODO: return 0 on failure + case kFuncStopAnim: + debug(3, "kFuncStopAnim(%d)", param1); + for (Common::List<Animation *>::iterator i = _anims.begin(); i != _anims.end(); i++) { + if ((*i)->_id == param1) + stopAnimation(*i); + } + return 0; + case kFuncQueueScript: + debug(3, "kFuncQueueScript(%d, %d, %d)", param1, param2, param3); + _queuedScripts[param1]._baseTime = _system->getMillis(); + _queuedScripts[param1]._duration = 10 * param2; + _queuedScripts[param1]._count = 0xffffffff; + _queuedScripts[param1]._scriptId = param3; + return 0; + case kFuncDequeueScript: + debug(3, "kFuncDequeueScript(%d)", param1); + _queuedScripts[param1]._count = 0; + _queuedScripts[param1]._scriptId = 0; + return 0; + case kFuncSetCursor: + debug(3, "kSetCursor(%d, (%d, %d))", param1, param2, param3); + { + uint16 oldCursor = _mouseSpriteId; + setCursor(param1, Common::Point(param2, param3)); + return oldCursor; + } + case kFuncGetCursor: + debug(3, "kFuncGetCursor()"); + return _mouseSpriteId; + case kFuncShowCursor: + debug(3, "kFuncShowCursor()"); + setCursorVisible(true); + return 0; + case kFuncHideCursor: + debug(3, "kFuncHideCursor()"); + setCursorVisible(false); + return 0; + case kFuncActivateButton: + debug(3, "kFuncActivateButton(%d)", param1); + for (Common::List<Library>::iterator l = _libraries.begin(); l != _libraries.end(); l++) { + for (Common::List<Button>::iterator i = l->_buttons.begin(); i != l->_buttons.end(); i++) { + if (i->_id != param1) + continue; + i->_active = true; + } + } + onMouseMove(_lastMousePos); + return 1; + case kFuncDeactivateButton: + debug(3, "kFuncDeactivateButton(%d)", param1); + for (Common::List<Library>::iterator l = _libraries.begin(); l != _libraries.end(); l++) { + for (Common::List<Button>::iterator i = l->_buttons.begin(); i != l->_buttons.end(); i++) { + if (i->_id != param1) + continue; + i->_active = false; + } + } + onMouseMove(_lastMousePos); + return 1; + case kFuncNewPage: + debug(3, "kFuncNewPage(%d, %d)", param1, param2); + _pendingPageChanges.push_back(PendingPageChange(param1, true)); + _pendingPageChanges.push_back(PendingPageChange(param2, false)); + return 1; + case kFuncLoadPage: + debug(3, "kFuncLoadPage(%d)", param1); + _pendingPageChanges.push_back(PendingPageChange(param1, false)); + return 1; + case kFuncUnloadPage: + debug(3, "kFuncUnloadPage(%d)", param1); + _pendingPageChanges.push_back(PendingPageChange(param1, true)); + return 1; + case kFuncSetPalette: + // TODO: return 0 if not disabling (0) and doesn't exist + debug(4, "kFuncSetPalette(%d, %d)", param1, param2); + // this seems only needed for a script bug in the Gregory credits, sigh + if ((uint16)param2 > 100) + param2 = 100; + loadCTBL(param1, param2); + return 1; + case kFuncSaveVars: + debug(3, "kFuncSaveVars(%d)", param1); + { + Common::String filename = _targetName + Common::String::format(".%03d", param1); + Common::WriteStream *stream = _saveFileMan->openForSaving(filename); + for (uint i = 0; i < 1000; i++) { + stream->writeUint16LE(_vars[i]); + } + delete stream; + } + return 1; + case kFuncLoadVars: + debug(3, "kFuncLoadVars(%d, %d, %d)", param1, param2, param3); + { + Common::String filename = _targetName + Common::String::format(".%03d", param1); + Common::SeekableReadStream *stream = _saveFileMan->openForLoading(filename); + if (!stream) { + if (!_bookIni.hasKey(Common::String::format("%d", param1), "Data")) + return 0; + filename = getFilename("Data", param1); + Common::File *file = new Common::File(); + if (!file->open(filename)) + error("couldn't open '%s' to get vars id '%d'", filename.c_str(), param1); + stream = file; + } + if (param3 == 0) + param3 = 1000; + else + param3 = param3; + if (param2 < 0 || param3 < 0 || param2 + param3 > 1000) + error("can't read %d entries into %d from file '%s' for vars id '%d'", param3, param2, filename.c_str(), param1); + stream->skip(param2 * 2); + for (uint i = 0; i < (uint)param3; i++) { + if (stream->pos() + 1 > stream->size()) + break; + _vars[param2 + i] = stream->readUint16LE(); + } + delete stream; + } + return 1; + case kFuncQueueScriptOnce: + debug(3, "kFuncQueueScriptOnce(%d, %d, %d)", param1, param2, param3); + _queuedScripts[param1]._baseTime = _system->getMillis(); + _queuedScripts[param1]._duration = 10 * param2; + _queuedScripts[param1]._count = 1; + _queuedScripts[param1]._scriptId = param3; + return 0; + case kFuncGetMousePos: + debug(3, "kFuncGetMousePos(%d, %d)", param1, param2); + _vars[param1] = _lastMousePos.x; + _vars[param2] = _lastMousePos.y; + return 0; + case kFuncChangeBackground: + debug(3, "kFuncChangeBackground(%d)", param1); + // TODO: return 1 if background existed, else 0 + setBackground(param1); + return 1; + case kFuncSetBackgroundColor: + // TODO + warning("ignoring kFuncSetBackgroundColor(%d)", param1); + return 0; + case kFuncClearSprites: + debug(3, "kFuncClearSprites()"); + removeSprite(0, 0); + return 0; + case kFuncAddSprite: + { + Common::Point pos(_vars[param3], _vars[param3 + 1]); + int16 zorder = _vars[param3 + 2]; + debug(3, "kFuncAddSprite(%d, %d, [%d = (%d, %d), %d])", param1, param2, param3, pos.x, pos.y, zorder); + addSprite(param1, param2, zorder, pos); + } + return 0; + case kFuncRemoveSprite: + debug(3, "kFuncRemoveSprite(%d, %d)", param1, param2); + removeSprite(param1, param2); + return 0; + case kFuncQuit: + debug(3, "kFuncQuit()"); + quitGame(); + return 0; + case kFuncSaveData: + // TODO + warning("ignoring kFuncSaveData(%d, %d, %d)", param1, param2, param3); + return 1; + case kFuncLoadData: + debug(3, "kFuncLoadData(%d, %d, %d)", param1, param2, param3); + { + Common::String filename = getFilename("Data", param1); + Common::File *file = new Common::File(); + if (!file->open(filename)) + error("couldn't open '%s' to get data id '%d'", filename.c_str(), param1); + if (param3 == 0) + param3 = 1000; + else + param3 = param3; + if (param2 < 0 || param3 < 0 || param2 + param3 > 1000) + error("can't read %d entries into %d from file '%s' for data id '%d'", param3, param2, filename.c_str(), param1); + for (uint i = 0; i < (uint)param3; i++) { + if (file->pos() + 1 > file->size()) + break; + _vars[param2 + i] = file->readUint16LE(); + } + delete file; + } + return 1; + case kFuncGetSpriteSize: + debug(3, "kFuncGetSpriteSize(%d, %d, %d)", param1, param2, param3); + int16 width, height; + width = 0; + height = 0; + { + Common::SeekableReadStream *stream = getStreamForSprite(param1); + if (stream) { + stream->readUint16LE(); + height = stream->readSint16LE(); + width = stream->readSint16LE(); + delete stream; + } + } + _vars[param2] = width; + _vars[param3] = height; + return 0; + default: + error("unknown scriptFuncCall %d(%d, %d, %d)", (uint32)id, param1, param2, param3); + } +} + +} // End of namespace Composer diff --git a/engines/engines.mk b/engines/engines.mk index dc09fbd54e..de11496b7b 100644 --- a/engines/engines.mk +++ b/engines/engines.mk @@ -31,6 +31,11 @@ DEFINES += -DENABLE_CINE=$(ENABLE_CINE) MODULES += engines/cine endif +ifdef ENABLE_COMPOSER +DEFINES += -DENABLE_COMPOSER=$(ENABLE_COMPOSER) +MODULES += engines/composer +endif + ifdef ENABLE_CRUISE DEFINES += -DENABLE_CRUISE=$(ENABLE_CRUISE) MODULES += engines/cruise |