/* 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. * * $URL$ * $Id$ * */ #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 "common/memstream.h" #include "graphics/cursorman.h" #include "graphics/surface.h" #include "graphics/pixelformat.h" #include "graphics/palette.h" #include "engines/util.h" #include "engines/advancedDetector.h" #include "audio/audiostream.h" #include "audio/decoders/raw.h" #include "composer/composer.h" #include "composer/resource.h" namespace Composer { // bitmap compression types enum { kBitmapUncompressed = 0, kBitmapSpp32 = 1, kBitmapSLW8 = 3, kBitmapRLESLWM = 4, kBitmapSLWM = 5 }; // 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, kOpNotPositive = 0xF, kOpSqrt = 0x10, kOpRandom = 0x11, kOpExecuteScript = 0x12, kOpCallFunc = 0x13, kOpBoolLessThanEq = 0x14, kOpBoolLessThan = 0x15, kOpBoolGreaterThanEq = 0x16, kOpBoolGreaterThan = 0x17, kOpBoolEqual = 0x18, kOpBoolNotEqual = 0x19, kOpSaveArgs = 0x1A, kOpRestoreArgs = 0x1B, kOpSetReturnValue = 0x20, kOpLessThanEq = 0x21, kOpLessThan = 0x22, kOpGreaterThanEq = 0x23, kOpGreaterThan = 0x24, 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 }; 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); } // TODO: params: x, y, event param for done 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.word6 = _stream->readUint16LE(); entry.dword0 = _stream->readUint16LE(); entry.counter = 0; entry.word10 = 0; debug(8, "anim entry: %04x, %04x, %04x", entry.op, entry.word6, entry.dword0); _entries.push_back(entry); } _offset = _stream->pos(); } Animation::~Animation() { delete _stream; } void Animation::seekToCurrPos() { _stream->seek(_offset, SEEK_SET); } 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); //uint32 baseOffset = _offset; 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); } 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.height() / 2; int16 b = _rect.width() / 2; if (!a || !b) return false; Common::Point adjustedPos = pos - Common::Point(_rect.left + a, _rect.top + b); return ((adjustedPos.x*adjustedPos.x)/a*2 + (adjustedPos.y*adjustedPos.y)/b*2 < 1); } case kButtonSprites: return false; default: error("internal error (button type %d)", _type); } } 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::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::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::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(1, 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.word10) continue; if (localOnly) { if (pipesOnly) continue; // TODO: stop audio if needed if (entry.op != 4) continue; removeSprite(entry.word10, anim->_id); } else { if (entry.op != 3) continue; for (Common::List::iterator i = _anims.begin(); i != _anims.end(); i++) { if ((*i)->_id == entry.word10) stopAnimation(*i); } } } // kill any pipe owned by the animation for (Common::List::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, bool bufferingOnly) { Common::SeekableReadStream *stream = NULL; if (!bufferingOnly && hasResource(ID_WAVE, id)) { stream = getResource(ID_WAVE, id); } else { for (Common::List::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) // FIXME: deal with word6 (priority) 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); delete stream; if (!_mixer->isSoundHandleActive(_soundHandle)) _mixer->playStream(Audio::Mixer::kSFXSoundType, &_soundHandle, _audioStream); } void ComposerEngine::processAnimFrame() { for (Common::List::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(2, 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 != 1) 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 { runEvent(event, anim->_id, 0, 0); } } } } for (Common::List::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 == 1)) continue; foundWait = true; if (entry.counter) { entry.counter--; if ((entry.op == 2) && entry.word10) { debug(4, "anim: continue play wave %d", entry.word10); playWaveForAnim(entry.word10, 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 1: // TODO warning("ignoring tingie (%d)", data); break; case 2: debug(4, "anim: play wave %d", data); playWaveForAnim(data, false); break; case 3: debug(4, "anim: play anim %d", data); playAnimation(data, anim->_basePos.x, anim->_basePos.y, 1); break; case 4: if (!data || (entry.word10 && (data != entry.word10))) { debug(4, "anim: erase sprite %d", entry.word10); removeSprite(entry.word10, 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.dword0) animId = 0; debug(4, "anim: draw sprite %d at (relative) %d,%d", data, x, y); bool wasVisible = spriteVisible(data, animId); addSprite(data, animId, entry.word6, 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 != 4) continue; if (anim->_entries[k].word10 == data) anim->_entries[k].word10 = 1; } } } break; default: warning("unknown anim op %d", entry.op); } entry.word10 = data; } } } } for (Common::List::iterator j = _pipes.begin(); j != _pipes.end(); j++) { Pipe *pipe = *j; pipe->nextFrame(); } } bool ComposerEngine::spriteVisible(uint16 id, uint16 animId) { for (Common::List::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; } void 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::iterator i = _sprites.begin(); i != _sprites.end(); i++) { if (i->_id != id) continue; if (i->_animId && animId && (i->_animId != animId)) continue; // if the zordering is identical, modify it in-place if (i->_zorder == zorder) { i->_animId = animId; i->_pos = pos; return; } // 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)) { warning("ignoring addSprite on invalid sprite %d", id); return; } } for (Common::List::iterator i = _sprites.begin(); i != _sprites.end(); i++) { if (sprite._zorder <= i->_zorder) continue; // insert *before* this sprite _sprites.insert(i, sprite); return; } _sprites.push_back(sprite); } void ComposerEngine::removeSprite(uint16 id, uint16 animId) { for (Common::List::iterator i = _sprites.begin(); i != _sprites.end(); i++) { if (id && i->_id != id) continue; if (i->_animId && animId && (i->_animId != animId)) continue; i->_surface.free(); i = _sprites.reverse_erase(i); if (id) break; } } const Sprite *ComposerEngine::getSpriteAtPos(const Common::Point &pos) { for (Common::List::iterator i = _sprites.begin(); i != _sprites.end(); i++) { if (i->contains(pos)) return &(*i); } return NULL; } const Button *ComposerEngine::getButtonFor(const Sprite *sprite, const Common::Point &pos) { for (Common::List