/* 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