/* 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 3 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, see . * */ #include "dgds/head.h" #include "dgds/dgds.h" #include "dgds/image.h" #include "dgds/includes.h" #include "dgds/sound_raw.h" #include "dgds/scene.h" #include "dgds/dialog.h" #include "dgds/ads.h" #include "graphics/cursorman.h" namespace Dgds { ImageFlipMode Conversation::_lastHeadFrameFlipMode = kImageFlipNone; int16 Conversation::_lastHeadFrameX = 0; int16 Conversation::_lastHeadFrameY = 0; void TalkDataHead::drawHead(Graphics::ManagedSurface &dst, const TalkData &data) const { uint drawtype = _drawType ? _drawType : 1; // Use specific head shape if available (eg, in Willy Beamish), if not use talk data shape Common::SharedPtr img = _shape; if (!img) img = data._shape; switch (drawtype) { case 1: if (img) drawHeadType1(dst, *img); break; case 2: if (img) drawHeadType2(dst, *img); break; case 3: if (DgdsEngine::getInstance()->getGameId() == GID_WILLY) drawHeadType3Beamish(dst, data); else if (img) drawHeadType3(dst, *img); break; default: error("Unsupported head draw type %d", drawtype); } } void TalkDataHead::drawHeadType1(Graphics::ManagedSurface &dst, const Image &img) const { Common::Rect r = _rect.toCommonRect(); dst.fillRect(r, _drawCol); r.grow(-1); dst.fillRect(r, _drawCol == 0 ? 15 : 0); r.left += 2; r.top += 2; const int x = _rect.x; const int y = _rect.y; if (img.isLoaded()) { for (const auto &frame : _headFrames) { img.drawBitmap(frame._frameNo & 0xff, x + frame._xoff, y + frame._yoff, r, dst); } } } void TalkDataHead::drawHeadType2(Graphics::ManagedSurface &dst, const Image &img) const { if (!img.isLoaded()) return; const Common::Rect r = _rect.toCommonRect(); for (const auto &frame : _headFrames) { img.drawBitmap(frame._frameNo & 0xff, r.left + frame._xoff, r.top + frame._yoff, r, dst); } } void TalkDataHead::drawHeadType3Beamish(Graphics::ManagedSurface &dst, const TalkData &data) const { const Common::Rect r = _rect.toCommonRect(); Common::Rect fillRect(r); fillRect.grow(-1); bool bgDone = false; for (const auto &frame : _headFrames) { int frameNo = frame._frameNo & 0x7fff; bool useHeadShape = frame._frameNo & 0x8000; Common::SharedPtr img = useHeadShape ? _shape : data._shape; if (!bgDone && img) { dst.frameRect(r, 8); dst.fillRect(fillRect, _drawCol); bgDone = true; } ImageFlipMode flip = kImageFlipNone; // Yes, the numerical values are reversed here (1 -> 2 and 2 -> 1). // The head flip flags are reversed from the image draw flags. if (frame._flipFlags & 1) flip = static_cast(flip | kImageFlipH); if (frame._flipFlags & 2) flip = static_cast(flip | kImageFlipV); // Slight hack from original CD version - record the flip mode and offset // for this frame and use it for drawing the head sprites in the script. if (flip != Conversation::_lastHeadFrameFlipMode) debug(10, "Changing CDS head flip mode %d -> %d", Conversation::_lastHeadFrameFlipMode, flip); Conversation::_lastHeadFrameFlipMode = flip; Conversation::_lastHeadFrameX = frame._xoff; Conversation::_lastHeadFrameY = frame._yoff; if (!img || !img->isLoaded() || frameNo >= img->loadedFrameCount()) continue; img->drawBitmap(frameNo, r.left + frame._xoff, r.top + frame._yoff, fillRect, dst); } } void TalkDataHead::drawHeadType3(Graphics::ManagedSurface &dst, const Image &img) const { Common::Rect r = _rect.toCommonRect(); dst.fillRect(r, 0); if (!img.isLoaded()) return; for (const auto &frame : _headFrames) { int frameNo = frame._frameNo; if (frameNo < img.loadedFrameCount()) img.drawBitmap(frameNo, r.left + frame._xoff, r.top + frame._yoff, r, dst); else dst.fillRect(r, 4); } } void TalkDataHead::clearHead() { warning("TODO: Clear head"); _flags = static_cast(_flags & ~(kHeadFlagFinished | kHeadFlag8 | kHeadFlag10 | kHeadFlagVisible)); /* This seems to just be a "needs redraw" flag, but we always redraw for (auto tds : _talkData) { for (auto h : tds._heads) { if ((h._flags & kHeadFlagVisible) && !(h._flags & (kHeadFlag8 | kHeadFlag10 | kHeadFlag80))) { if (h._rect.toCommonRect().intersects(head._rect.toCommonRect())) { h._flags = static_cast(h._flags | kHeadFlag4); } } } } */ } void TalkData::clearVisibleHeads() { for (auto &head : _heads) { if (head._flags & kHeadFlagVisible) head.clearHead(); } } void TalkData::drawAndUpdateVisibleHeads(Graphics::ManagedSurface &dst) { for (auto &h : _heads) { if (h._flags & kHeadFlagVisible) { if (!(h._flags & kHeadFlagOpening)) { h.drawHead(dst, *this); } else { h._flags = static_cast(h._flags & ~kHeadFlagOpening); } } } } bool TalkData::hasVisibleHead() const { for (const auto &h : _heads) { if (h._flags & kHeadFlagVisible && !(h._flags & kHeadFlagOpening)) return true; } return false; } CDSTTMInterpreter::CDSTTMInterpreter(DgdsEngine *vm) : TTMInterpreter(vm) { _storedAreaBuffer.create(SCREEN_WIDTH, SCREEN_HEIGHT, Graphics::PixelFormat::createFormatCLUT8()); } void CDSTTMInterpreter::handleOperation(TTMEnviro &env_, TTMSeq &seq, uint16 op, byte count, const int16 *ivals, const Common::String &sval, const Common::Array &pts) { CDSTTMEnviro &env = static_cast(env_); switch (op) { case 0x0080: // FREE SHAPE env._scriptShapes[0].reset(); break; case 0x0110: // PURGE void break; case 0x0FF0: // REFRESH break; case 0x1020: { // SET DELAY: i:int [0..n] // TODO: Probably should do this accounting (as well as timeCut and dialogs) // in game frames, not millis. int16 delayMillis = (int16)round(ivals[0] * MS_PER_FRAME); env._cdsDelay = MAX(env._cdsDelay, delayMillis); break; } case 0x1050: // SELECT BMP: id:int [0] seq._currentBmpId = ivals[0]; break; case 0x1060: // SELECT PAL: id:int [0] seq._currentPalId = ivals[0]; break; case 0x1100: // SET_SCENE: i:int [1..n] case 0x1110: // SET_SCENE: i:int [1..n] break; case 0x2000: // SET (DRAW) COLORS: fgcol,bgcol:int [0..255] seq._drawColFG = static_cast(ivals[0]); // aka Line Color seq._drawColBG = static_cast(ivals[1]); // aka Fill Color break; case 0x3200: // CDS FIND GOTO TARGET frameno seq._gotoFrame = findGOTOTarget(env, seq, ivals[0]); break; case 0x3300: // CDS GOSUB - first 2 args are ignored by original if (!env._cdsJumped && seq._gotoFrame + ivals[2] != seq._currentFrame && seq._gotoFrame >= 0) { env._cdsJumped = true; int64 prevPos = env.scr->pos(); int16 currentFrame = seq._currentFrame; env.scr->seek(env._frameOffsets[seq._gotoFrame + ivals[2]]); seq._currentFrame = seq._gotoFrame; run(env, seq); seq._currentFrame = currentFrame; env.scr->seek(prevPos); env._cdsJumped = false; } break; case 0x4200: { // STORE AREA: x,y,w,h:int [0..n] ; makes this area of foreground persist in the next frames. if (env._cdsDidStoreArea) // this is a one-shot op break; if (env._storedAreaRect.width * 2 < (int)ivals[2]) { env._storedAreaRect = DgdsRect(ivals[0] + env._xOff, ivals[1] + env._yOff, ivals[2], ivals[3]); } const Common::Rect rect = env._storedAreaRect.toCommonRect(); _storedAreaBuffer.blitFrom(_vm->_compositionBuffer, rect, rect); env._cdsDidStoreArea = true; break; } case 0xa500: // DRAW SPRITE: x,y,frameno,bmpno:int [-n,+n] case 0xa510: // DRAW SPRITE FLIP V x,y:int case 0xa520: // DRAW SPRITE FLIP H: x,y:int case 0xa530: { // DRAW SPRITE FLIP HV: x,y,frameno,bmpno:int [-n,+n] if (!env._scriptShapes[0]) { warning("CDS: Trying to draw after script shape freed"); break; } ImageFlipMode flip = Conversation::_lastHeadFrameFlipMode; int16 x = ivals[0] + env._xOff; int16 y = ivals[1] + env._yOff; int16 frameno = ivals[2]; int16 bmpNo = ivals[3]; Common::SharedPtr img = env._scriptShapes[bmpNo]; if (flip & kImageFlipH) x = env._xOff + (env._scriptShapes[0]->width(0) - ivals[0] - img->width(frameno)); if (frameno >= img->loadedFrameCount()) { warning("CDS script tried to draw frame %d but img only has %d", frameno, img->loadedFrameCount()); } else { img->drawBitmap(frameno, x, y, seq._drawWin, _vm->_compositionBuffer, flip, img->width(frameno), img->height(frameno)); } break; } case 0xc220: // PLAY RAW SFX if (env._cdsPlayedSound) // this is a one-shot op break; if (!env._soundRaw) { warning("TODO: Trying to play raw SFX but nothing loaded"); } else { env._soundRaw->play(); env._cdsPlayedSound = true; } break; case 0xc250: { // SYNC RAW SFX uint16 hi = (uint16)ivals[1]; uint16 lo = (uint16)ivals[0]; uint32 offset = ((uint32)hi << 16) + lo; // The offset is in bytes of the sample. uint32 playedOffset = env._soundRaw->playedOffset(); if (playedOffset < offset) { // Not played to this point yet. Add a delay until that point. env._cdsDelay = MAX(env._cdsDelay, (int16)(1000 * (offset - playedOffset) / 11025)); debug(10, "CDS SYNC SFX: played %d wait for %d (%d ms)", playedOffset, offset, env._cdsDelay); } break; } case 0xa100: // DRAW FILLED RECT case 0xa110: // DRAW EMPTY RECT x1,y1,x2,y2:int // Appear in the scripts but not implemented in the original. break; case 0xc200: // ??? SFX: ??,?? - not implemented in willy, ignore? case 0xc210: // LOAD RAW SFX: filename:str case 0xf010: // LOAD SCR: filename:str case 0xf020: // LOAD BMP: filename:str case 0xf050: // LOAD PAL: filename:str // Ignore all these in CDS scripts. The original implements them but only to load // patch data outside of the CDS files. That data isn't present in the final // games so we can just ignore the opcode. break; default: if (count < 15) warning("Unimplemented CDS TTM opcode: 0x%04X (%s, %d args) (ivals: %d %d %d %d)", op, ttmOpName(op), count, ivals[0], ivals[1], ivals[2], ivals[3]); else warning("Unimplemented CDS TTM opcode: 0x%04X (%s, sval: %s)", op, ttmOpName(op), sval.c_str()); break; } } Conversation::~Conversation() { unloadData(); } void Conversation::unloadData() { debug(10, "CDS: Unloading data (%d,%d)", _dlgNum, _dlgFileNum); _ttmScript.reset(); _ttmEnv._scriptShapes[0].reset(); if (_ttmEnv._soundRaw) _ttmEnv._soundRaw->stop(); _ttmEnv = CDSTTMEnviro(); _loadState = 0; _lastHeadFrameX = 0; _lastHeadFrameY = 0; } void Conversation::clear() { _dlgNum = -1; _dlgFileNum = -1; _subNum = -1; _finished = false; _haveHeadData = false; _stopScript = false; } bool Conversation::isForDlg(const Dialog *dlg) const { return dlg && dlg->_num == _dlgNum && dlg->_fileNum == _dlgFileNum; } void Conversation::loadData(uint16 dlgFileNum, uint16 dlgNum, int16 sub, bool haveHeadData) { unloadData(); clear(); DgdsEngine *engine = DgdsEngine::getInstance(); // These files are only present in Willy Beamish CD version if (engine->getGameId() != GID_WILLY) return; _dlgNum = dlgNum; _dlgFileNum = dlgFileNum; _subNum = sub; _nextExecMs = 0; _runTempFrame = -1; _tempFrameNum = 0; _thisFrameMs = 0; _stopScript = false; _haveHeadData = haveHeadData; if (!haveHeadData) _drawRect = DgdsRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); ResourceManager *resourceManager = engine->getResourceManager(); Decompressor *decompressor = engine->getDecompressor(); Common::String fname; if (sub >= 0) { assert(sub < 26); fname = Common::String::format("F%dB%d%c.CDS", dlgFileNum, dlgNum, 'A' + sub); } else { fname = Common::String::format("F%dB%d.CDS", dlgFileNum, dlgNum); } if (!resourceManager->hasResource(fname)) return; debug(10, "CDS: Load CDS resource %s", fname.c_str()); // The scripts are designed so the resources are patchable, but by default // they use the sound and image data from the CDS file. _ttmEnv._soundRaw.reset(new SoundRaw(resourceManager, decompressor)); _ttmEnv._soundRaw->load(fname); _ttmEnv._scriptShapes[0].reset(new Image(resourceManager, decompressor)); _ttmEnv._scriptShapes[0]->loadBitmap(fname); _ttmScript.reset(new CDSTTMInterpreter(engine)); _ttmScript->load(fname, _ttmEnv); _ttmScript->findAndAddSequences(_ttmEnv, _ttmSeqs); _loadState = 1; // Then run START for (const auto &tag : _ttmEnv._tags) { if (tag._value.equalsIgnoreCase("START")) _ttmEnv._cdsFrame = tag._key; } _tempFrameNum = _ttmScript->findGOTOTarget(_ttmEnv, *_ttmSeqs[0], _ttmEnv._cdsFrame); _ttmEnv._cdsFrame = _tempFrameNum; // Always run frame 1 on init. runScriptFrame(1); } bool Conversation::runScriptFrame(int16 frameNum) { if (!_ttmScript || frameNum >= _ttmEnv._totalFrames) return false; _ttmEnv.scr->seek(_ttmEnv._frameOffsets[frameNum]); TTMSeq *seq = _ttmSeqs[0].get(); seq->_currentFrame = frameNum; debug(10, "CDS: Running TTM sequence %d frame %d current millis %d", seq->_seqNum, seq->_currentFrame, _thisFrameMs); seq->_drawWin = _drawRect.toCommonRect(); return _ttmScript->run(_ttmEnv, *seq); } void Conversation::checkAndRunScript() { if (!_ttmScript || _finished) return; DgdsEngine *engine = DgdsEngine::getInstance(); // Always add the stored buffer first. Copy the whole thing to overwrite other state. engine->_compositionBuffer.transBlitFrom(_ttmScript->getStoredAreaBuffer()); if (_runTempFrame) { runScriptFrame(_tempFrameNum); } runScriptFrame(_ttmEnv._cdsFrame); if (_ttmEnv._cdsDelay > 0) { _nextExecMs = _thisFrameMs + _ttmEnv._cdsDelay; debug(10, "CDS: This fame %d. Next frame will be on or after %d", _thisFrameMs, _nextExecMs); _ttmEnv._cdsDelay = -1; } else { _nextExecMs = 0; } } void Conversation::incrementFrame() { if (!_ttmScript) return; // TODO: check load type 2 here? if (_loadState == 1 && _ttmEnv._scriptShapes[0] && _ttmEnv._scriptShapes[0]->loadedFrameCount() > 0) { _ttmEnv._storedAreaRect.x = _drawRect.x; _ttmEnv._storedAreaRect.y = _drawRect.y; _ttmEnv._storedAreaRect.width = _ttmEnv._scriptShapes[0]->getFrames()[0]->w + 8; _ttmEnv._storedAreaRect.height = _ttmEnv._scriptShapes[0]->getFrames()[0]->h; _loadState = 2; } if (_runTempFrame) _runTempFrame--; if (!_nextExecMs || _thisFrameMs >= _nextExecMs) { debug(10, "CDS: Increment frame %d -> %d", _ttmEnv._cdsFrame, _ttmEnv._cdsFrame + 1); _ttmEnv._cdsFrame++; } } bool Conversation::isScriptRunning() { // HACK: Keep viewing blueprints from inventory running until // interrupted.. other dialogs should stop when they // have finished or their sounds or hit the end. bool isBlueprints = (_dlgNum == 16 && _dlgFileNum == 67 && _subNum == -1); return (_ttmScript && ((isBlueprints || _ttmEnv._soundRaw->isPlaying()) || (_ttmEnv._cdsFrame < _ttmEnv._totalFrames) )); } void Conversation::pumpMessages() { Common::Event ev; DgdsEngine *engine = DgdsEngine::getInstance(); while (engine->getEventManager()->pollEvent(ev)) { if (ev.type == Common::EVENT_LBUTTONDOWN || ev.type == Common::EVENT_RBUTTONDOWN || ev.type == Common::EVENT_KEYDOWN) { _stopScript = true; engine->getScene()->setIgnoreMouseUp(); } } } void Conversation::runScript() { if (!_ttmScript) return; // // If we have head data, run the script and don't return until it's // finished - stop all other activity. // // If not, just run the script at the same time as it's supposed to animate over // the top of the other game movements. // if (_haveHeadData) runScriptExclusive(); else runScriptStep(); } void Conversation::runScriptStep() { DgdsEngine *engine = DgdsEngine::getInstance(); if (_runTempFrame == -1) _runTempFrame = 2; if (!isScriptRunning()) return; _thisFrameMs = engine->getThisFrameMs(); if (!_nextExecMs || _nextExecMs <= _thisFrameMs) { incrementFrame(); checkAndRunScript(); } } void Conversation::runScriptExclusive() { DgdsEngine *engine = DgdsEngine::getInstance(); engine->disableKeymapper(); _nextExecMs = 0; _drawRect.x += _lastHeadFrameX; _drawRect.y += _lastHeadFrameY; _ttmEnv._xOff = _drawRect.x; _ttmEnv._yOff = _drawRect.y; _runTempFrame = 2; if (_drawRect.width == 0) { _drawRect.width = SCREEN_WIDTH; _drawRect.height = SCREEN_HEIGHT; } int frameCount = 0; uint32 startMillis = g_system->getMillis(); DgdsEngine::dumpFrame(engine->_compositionBuffer, "cds-comp-before-script"); engine->getScene()->checkDialogActive(); // update here to make sure the dialog gets drawn. engine->getScene()->drawAndUpdateDialogs(&engine->_compositionBuffer); DgdsEngine::dumpFrame(engine->_compositionBuffer, "cds-comp-before-script-with-dlg"); _ttmScript->getStoredAreaBuffer().blitFrom(engine->_compositionBuffer); CursorMan.showMouse(false); g_system->copyRectToScreen(engine->_compositionBuffer.getPixels(), SCREEN_WIDTH, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); g_system->updateScreen(); // If we have head data we should dim the palette engine->dimPalForWillyDialog(_haveHeadData); debug(10, "CDS: Start execution of (%d,%d) - frame %d maxFrame %d", _dlgNum, _dlgFileNum, _ttmEnv._cdsFrame, _ttmEnv._totalFrames); while (isScriptRunning() && !engine->shouldQuit()) { _thisFrameMs = g_system->getMillis(); pumpMessages(); if (_stopScript) break; if (!_nextExecMs || _nextExecMs <= _thisFrameMs) { incrementFrame(); checkAndRunScript(); // Redraw active dialogs eg to make sure thought bubble dots are // over the moving heads engine->getScene()->drawAndUpdateDialogs(&engine->_compositionBuffer); Common::Rect r = _drawRect.toCommonRect(); r.clip(Common::Rect(SCREEN_WIDTH, SCREEN_HEIGHT)); const byte *srcPtr = (const byte *)engine->_compositionBuffer.getPixels() + r.top * SCREEN_WIDTH + r.left; g_system->copyRectToScreen(srcPtr, SCREEN_WIDTH, r.left, r.top, r.width(), r.height()); } g_system->updateScreen(); frameCount++; // Limit to 15 FPS static const int framesPerSecond = 15; uint32 thisFrameEndMillis = g_system->getMillis(); uint32 elapsedMillis = thisFrameEndMillis - startMillis; const uint32 targetMillis = (frameCount * 1000 / framesPerSecond); if (targetMillis > elapsedMillis) { while (targetMillis > elapsedMillis) { pumpMessages(); g_system->updateScreen(); g_system->delayMillis(5); elapsedMillis = g_system->getMillis() - startMillis; } } else if (targetMillis < elapsedMillis) { startMillis = thisFrameEndMillis; frameCount = 0; } } debug(10, "CDS: Finished execution of (%d,%d) - stop %s frame %d maxFrame %d", _dlgNum, _dlgFileNum, _stopScript ? "true" : "false", _ttmEnv._cdsFrame, _ttmEnv._totalFrames); CursorMan.showMouse(true); engine->enableKeymapper(); if (_ttmEnv._soundRaw) _ttmEnv._soundRaw->stop(); // If the dialog was cleared, set the force-clear flag in the scene. Otherwise, just set // a flag the scene can check when updating dialogs. if (_stopScript) engine->getScene()->setShouldClearDlg(); else _finished = true; unloadData(); } } // end namespace Dgds