scummvm/engines/sword1/animation.cpp

577 lines
18 KiB
C++
Raw Normal View History

/* 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/file.h"
#include "common/events.h"
#include "common/keyboard.h"
#include "common/textconsole.h"
#include "common/translation.h"
#include "sword1/sword1.h"
#include "sword1/animation.h"
#include "sword1/text.h"
#include "sword1/resman.h"
#include "common/str.h"
#include "common/system.h"
#include "graphics/palette.h"
#include "graphics/surface.h"
#include "gui/message.h"
2012-11-29 00:34:09 +00:00
#ifdef USE_MPEG2
#include "video/avi_decoder.h"
#endif
#ifdef USE_ZLIB
#include "video/dxa_decoder.h"
2012-11-29 00:34:09 +00:00
#endif
#include "video/psx_decoder.h"
#include "video/smk_decoder.h"
#include "engines/util.h"
namespace Sword1 {
2011-09-07 22:05:09 +00:00
static const char *const sequenceList[20] = {
2009-08-30 20:50:23 +00:00
"ferrari", // 0 CD2 ferrari running down fitz in sc19
"ladder", // 1 CD2 george walking down ladder to dig sc24->sc$
"steps", // 2 CD2 george walking down steps sc23->sc24
"sewer", // 3 CD1 george entering sewer sc2->sc6
"intro", // 4 CD1 intro sequence ->sc1
"river", // 5 CD1 george being thrown into river by flap & g$
"truck", // 6 CD2 truck arriving at bull's head sc45->sc53/4
"grave", // 7 BOTH george's grave in scotland, from sc73 + from sc38 $
"montfcon", // 8 CD2 monfaucon clue in ireland dig, sc25
"tapestry", // 9 CD2 tapestry room beyond spain well, sc61
"ireland", // 10 CD2 ireland establishing shot europe_map->sc19
"finale", // 11 CD2 grand finale at very end, from sc73
"history", // 12 CD1 George's history lesson from Nico, in sc10
"spanish", // 13 CD2 establishing shot for 1st visit to Spain, europe_m$
"well", // 14 CD2 first time being lowered down well in Spai$
"candle", // 15 CD2 Candle burning down in Spain mausoleum sc59
"geodrop", // 16 CD2 from sc54, George jumping down onto truck
"vulture", // 17 CD2 from sc54, vultures circling George's dead body
"enddemo", // 18 --- for end of single CD demo
"credits", // 19 CD2 credits, to follow "finale" sequence
};
// This is the list of the names of the PlayStation videos
// TODO: fight.str, flashy.str,
static const char *const sequenceListPSX[20] = {
"e_ferr1",
"ladder1",
"steps1",
"sewer1",
"e_intro1",
"river1",
"truck1",
"grave1",
"montfcn1",
"tapesty1",
"ireland1",
"e_fin1",
"e_hist1",
"spanish1",
"well1",
"candle1",
"geodrop1",
"vulture1",
"", // demo video not present
"" // credits are not a video
};
///////////////////////////////////////////////////////////////////////////////
// Basic movie player
///////////////////////////////////////////////////////////////////////////////
MoviePlayer::MoviePlayer(SwordEngine *vm, Text *textMan, ResMan *resMan, OSystem *system, Video::VideoDecoder *decoder, DecoderType decoderType)
: _vm(vm), _textMan(textMan), _resMan(resMan), _system(system), _textX(0), _textY(0), _textWidth(0), _textHeight(0), _textColor(1) {
_decoderType = decoderType;
_decoder = decoder;
_c1Color = _c2Color = _c3Color = _c4Color = 255;
_black = 0;
2004-03-21 18:49:04 +00:00
}
MoviePlayer::~MoviePlayer() {
delete _decoder;
2004-03-21 18:49:04 +00:00
}
/**
* Plays an animated cutscene.
* @param id the id of the file
*/
bool MoviePlayer::load(uint32 id) {
Common::String filename;
if (SwordEngine::_systemVars.showText) {
Common::File f;
filename = Common::String::format("%s.txt", sequenceList[id]);
2009-02-21 11:22:56 +00:00
if (f.open(filename)) {
Common::String line;
int lineNo = 0;
int lastEnd = -1;
_movieTexts.clear();
while (!f.eos() && !f.err()) {
line = f.readLine();
lineNo++;
if (line.empty() || line[0] == '#') {
continue;
}
const char *ptr = line.c_str();
// TODO: Better error handling
2008-12-01 21:35:17 +00:00
int startFrame = strtoul(ptr, const_cast<char **>(&ptr), 10);
int endFrame = strtoul(ptr, const_cast<char **>(&ptr), 10);
while (*ptr && Common::isSpace(*ptr))
ptr++;
if (startFrame > endFrame) {
warning("%s:%d: startFrame (%d) > endFrame (%d)", filename.c_str(), lineNo, startFrame, endFrame);
continue;
}
if (startFrame <= lastEnd) {
warning("%s:%d startFrame (%d) <= lastEnd (%d)", filename.c_str(), lineNo, startFrame, lastEnd);
continue;
}
int color = 0;
if (*ptr == '@') {
++ptr;
color = strtoul(ptr, const_cast<char **>(&ptr), 10);
while (*ptr && Common::isSpace(*ptr))
ptr++;
}
_movieTexts.push_back(MovieText(startFrame, endFrame, ptr, color));
lastEnd = endFrame;
}
}
}
switch (_decoderType) {
case kVideoDecoderDXA:
filename = Common::String::format("%s.dxa", sequenceList[id]);
break;
case kVideoDecoderSMK:
filename = Common::String::format("%s.smk", sequenceList[id]);
break;
case kVideoDecoderPSX:
filename = Common::String::format("%s.str", (_vm->_systemVars.isDemo) ? sequenceList[id] : sequenceListPSX[id]);
2012-11-29 00:34:09 +00:00
break;
case kVideoDecoderMP2:
filename = Common::String::format("%s.mp2", sequenceList[id]);
break;
}
2012-11-29 00:34:09 +00:00
// Need to switch to true color for PSX/MP2 videos
if (_decoderType == kVideoDecoderPSX || _decoderType == kVideoDecoderMP2)
initGraphics(g_system->getWidth(), g_system->getHeight(), nullptr);
2012-11-29 00:34:09 +00:00
if (!_decoder->loadFile(filename)) {
// Go back to 8bpp color
if (_decoderType == kVideoDecoderPSX || _decoderType == kVideoDecoderMP2)
initGraphics(g_system->getWidth(), g_system->getHeight());
return false;
2012-11-29 00:34:09 +00:00
}
2012-11-29 00:34:09 +00:00
// For DXA/MP2, also add the external sound file
if (_decoderType == kVideoDecoderDXA || _decoderType == kVideoDecoderMP2)
_decoder->addStreamFileTrack(sequenceList[id]);
_decoder->start();
return true;
}
void MoviePlayer::play() {
_textX = 0;
_textY = 0;
playVideo();
_textMan->releaseText(2, false);
_movieTexts.clear();
// It's tempting to call _screen->fullRefresh() here to restore the old
// palette. However, that causes glitches with DXA movies, where the
// previous location would be momentarily drawn, before switching to
// the new one. Work around this by setting the palette to black.
byte pal[3 * 256];
memset(pal, 0, sizeof(pal));
_system->getPaletteManager()->setPalette(pal, 0, 256);
}
void MoviePlayer::performPostProcessing(byte *screen) {
2012-11-29 00:34:09 +00:00
// TODO: We don't support displaying these in true color yet,
// nor using the PSX fonts to display subtitles.
2012-11-29 00:34:09 +00:00
if (_vm->isPsx() || _decoderType == kVideoDecoderMP2)
return;
if (!_movieTexts.empty()) {
if (_decoder->getCurFrame() == _movieTexts.front()._startFrame) {
_textMan->makeTextSprite(2, (const uint8 *)_movieTexts.front()._text.c_str(), 600, LETTER_COL);
FrameHeader *frame = _textMan->giveSpriteData(2);
_textWidth = _resMan->toUint16(frame->width);
_textHeight = _resMan->toUint16(frame->height);
_textX = 320 - _textWidth / 2;
_textY = 420 - _textHeight;
_textColor = _movieTexts.front()._color;
}
if (_decoder->getCurFrame() == _movieTexts.front()._endFrame) {
_textMan->releaseText(2, false);
_movieTexts.pop_front();
}
}
byte *src, *dst;
int x, y;
if (_textMan->giveSpriteData(2)) {
src = (byte *)_textMan->giveSpriteData(2) + sizeof(FrameHeader);
dst = screen + _textY * SCREEN_WIDTH + _textX * 1;
for (y = 0; y < _textHeight; y++) {
for (x = 0; x < _textWidth; x++) {
switch (src[x]) {
case BORDER_COL:
dst[x] = getBlackColor();
break;
case LETTER_COL:
dst[x] = findTextColor();
break;
}
}
src += _textWidth;
dst += SCREEN_WIDTH;
}
} else if (_textX && _textY) {
// If the frame doesn't cover the entire screen, we have to
// erase the subtitles manually.
int frameWidth = _decoder->getWidth();
int frameHeight = _decoder->getHeight();
int frameX = (_system->getWidth() - frameWidth) / 2;
int frameY = (_system->getHeight() - frameHeight) / 2;
dst = screen + _textY * _system->getWidth();
for (y = 0; y < _textHeight; y++) {
if (_textY + y < frameY || _textY + y >= frameY + frameHeight) {
memset(dst + _textX, getBlackColor(), _textWidth);
} else {
if (frameX > _textX)
memset(dst + _textX, getBlackColor(), frameX - _textX);
if (frameX + frameWidth < _textX + _textWidth)
memset(dst + frameX + frameWidth, getBlackColor(), _textX + _textWidth - (frameX + frameWidth));
}
dst += _system->getWidth();
}
_textX = 0;
_textY = 0;
}
}
bool MoviePlayer::playVideo() {
bool skipped = false;
uint16 x = (g_system->getWidth() - _decoder->getWidth()) / 2;
uint16 y = (g_system->getHeight() - _decoder->getHeight()) / 2;
while (!_vm->shouldQuit() && !_decoder->endOfVideo() && !skipped) {
if (_decoder->needsUpdate()) {
const Graphics::Surface *frame = _decoder->decodeNextFrame();
if (frame) {
if (_decoderType == kVideoDecoderPSX)
drawFramePSX(frame);
else
_vm->_system->copyRectToScreen(frame->getPixels(), frame->pitch, x, y, frame->w, frame->h);
}
if (_decoder->hasDirtyPalette()) {
2012-08-16 17:30:32 +00:00
_vm->_system->getPaletteManager()->setPalette(_decoder->getPalette(), 0, 256);
if (!_movieTexts.empty()) {
// Look for the best color indexes to use to display the subtitles
uint32 minWeight = 0xFFFFFFFF;
uint32 weight;
float c1Weight = 1e+30f;
float c2Weight = 1e+30f;
float c3Weight = 1e+30f;
float c4Weight = 1e+30f;
byte r, g, b;
float h, s, v, hd, hsvWeight;
const byte *palette = _decoder->getPalette();
// Color comparaison for the subtitles colors is done in HSL
// C1 color is used for George and is almost white (R = 248, G = 252, B = 248)
const float h1 = 0.333333f, s1 = 0.02f, v1 = 0.99f;
// C2 color is used for George as a narrator and is grey (R = 184, G = 188, B = 184)
const float h2 = 0.333333f, s2 = 0.02f, v2 = 0.74f;
// C3 color is used for Nicole and is rose (R = 200, G = 120, B = 184)
const float h3 = 0.866667f, s3 = 0.4f, v3 = 0.78f;
// C4 color is used for Maguire and is blue (R = 80, G = 152, B = 184)
const float h4 = 0.55f, s4 = 0.57f, v4 = 0.72f;
for (int i = 0; i < 256; i++) {
r = *palette++;
g = *palette++;
b = *palette++;
weight = 3 * r * r + 6 * g * g + 2 * b * b;
if (weight <= minWeight) {
minWeight = weight;
_black = i;
}
convertColor(r, g, b, h, s, v);
// C1 color
// It is almost achromatic (very low saturation) so the hue as litle impact on the color.
// Therefore use a low weight on hue and high weight on saturation.
hd = h - h1;
hd += hd < -0.5f ? 1.0f : hd > 0.5f ? -1.0f : 0.0f;
hsvWeight = 1.0f * hd * hd + 4.0f * (s - s1) * (s - s1) + 3.0f * (v - v1) * (v - v1);
if (hsvWeight <= c1Weight) {
c1Weight = hsvWeight;
_c1Color = i;
}
// C2 color
// Also an almost achromatic color so use the same weights as for C1 color.
hd = h - h2;
hd += hd < -0.5f ? 1.0f : hd > 0.5f ? -1.0f : 0.0f;
hsvWeight = 1.0f * hd * hd + 4.0f * (s - s2) * (s - s2) + 3.0f * (v - v2) * (v - v2);
if (hsvWeight <= c2Weight) {
c2Weight = hsvWeight;
_c2Color = i;
}
// C3 color
// A light rose. Use a high weight on the hue to get a rose.
// The color is a bit gray and the saturation has not much impact so use a low weight.
hd = h - h3;
hd += hd < -0.5f ? 1.0f : hd > 0.5f ? -1.0f : 0.0f;
hsvWeight = 4.0f * hd * hd + 1.0f * (s - s3) * (s - s3) + 2.0f * (v - v3) * (v - v3);
if (hsvWeight <= c3Weight) {
c3Weight = hsvWeight;
_c3Color = i;
}
// C4 color
// Blue. Use a hight weight on the hue to get a blue.
// The color is darker and more saturated than C3 and the saturation has more impact.
hd = h - h4;
hd += hd < -0.5f ? 1.0f : hd > 0.5f ? -1.0f : 0.0f;
hsvWeight = 5.0f * hd * hd + 3.0f * (s - s4) * (s - s4) + 2.0f * (v - v4) * (v - v4);
if (hsvWeight <= c4Weight) {
c4Weight = hsvWeight;
_c4Color = i;
}
}
}
}
Graphics::Surface *screen = _vm->_system->lockScreen();
performPostProcessing((byte *)screen->getPixels());
_vm->_system->unlockScreen();
_vm->_system->updateScreen();
}
Common::Event event;
while (_vm->_system->getEventManager()->pollEvent(event))
if ((event.type == Common::EVENT_KEYDOWN && event.kbd.keycode == Common::KEYCODE_ESCAPE) || event.type == Common::EVENT_LBUTTONUP)
skipped = true;
_vm->_system->delayMillis(10);
}
2012-11-29 00:34:09 +00:00
// Need to jump back to paletted color
if (_decoderType == kVideoDecoderPSX || _decoderType == kVideoDecoderMP2)
initGraphics(g_system->getWidth(), g_system->getHeight());
return !_vm->shouldQuit() && !skipped;
}
uint32 MoviePlayer::getBlackColor() {
2012-11-29 00:34:09 +00:00
return (_decoderType == kVideoDecoderPSX || _decoderType == kVideoDecoderMP2) ? g_system->getScreenFormat().RGBToColor(0x00, 0x00, 0x00) : _black;
}
uint32 MoviePlayer::findTextColor() {
2012-11-29 00:34:09 +00:00
if (_decoderType == kVideoDecoderPSX || _decoderType == kVideoDecoderMP2) {
// We're in true color mode, so return the actual colors
switch (_textColor) {
case 1:
return g_system->getScreenFormat().RGBToColor(248, 252, 248);
case 2:
return g_system->getScreenFormat().RGBToColor(184, 188, 184);
case 3:
return g_system->getScreenFormat().RGBToColor(200, 120, 184);
case 4:
return g_system->getScreenFormat().RGBToColor(80, 152, 184);
}
return g_system->getScreenFormat().RGBToColor(0xFF, 0xFF, 0xFF);
}
switch (_textColor) {
case 1:
return _c1Color;
case 2:
return _c2Color;
case 3:
return _c3Color;
case 4:
return _c4Color;
}
return _c1Color;
}
void MoviePlayer::convertColor(byte r, byte g, byte b, float &h, float &s, float &v) {
float varR = r / 255.0f;
float varG = g / 255.0f;
float varB = b / 255.0f;
float min = MIN(varR, MIN(varG, varB));
float max = MAX(varR, MAX(varG, varB));
v = max;
float d = max - min;
s = max == 0.0f ? 0.0f : d / max;
if (min == max) {
h = 0.0f; // achromatic
} else {
if (max == varR)
h = (varG - varB) / d + (varG < varB ? 6.0f : 0.0f);
else if (max == varG)
h = (varB - varR) / d + 2.0f;
else
h = (varR - varG) / d + 4.0f;
h /= 6.0f;
}
}
void MoviePlayer::drawFramePSX(const Graphics::Surface *frame) {
// The PSX videos have half resolution
Graphics::Surface scaledFrame;
scaledFrame.create(frame->w, frame->h * 2, frame->format);
for (int y = 0; y < scaledFrame.h; y++)
memcpy(scaledFrame.getBasePtr(0, y), frame->getBasePtr(0, y / 2), scaledFrame.w * scaledFrame.format.bytesPerPixel);
uint16 x = (g_system->getWidth() - scaledFrame.w) / 2;
uint16 y = (g_system->getHeight() - scaledFrame.h) / 2;
_vm->_system->copyRectToScreen(scaledFrame.getPixels(), scaledFrame.pitch, x, y, scaledFrame.w, scaledFrame.h);
scaledFrame.free();
}
///////////////////////////////////////////////////////////////////////////////
// Factory function for creating the appropriate cutscene player
///////////////////////////////////////////////////////////////////////////////
MoviePlayer *makeMoviePlayer(uint32 id, SwordEngine *vm, Text *textMan, ResMan *resMan, OSystem *system) {
Common::String filename;
// For the PSX version, we'll try the PlayStation stream files
if (vm->isPsx()) {
// The demo uses the normal file names
filename = ((vm->_systemVars.isDemo) ? Common::String(sequenceList[id]) : Common::String(sequenceListPSX[id])) + ".str";
if (Common::File::exists(filename)) {
#ifdef USE_RGB_COLOR
// All BS1 PSX videos run the videos at 2x speed
Video::VideoDecoder *psxDecoder = new Video::PSXStreamDecoder(Video::PSXStreamDecoder::kCD2x);
return new MoviePlayer(vm, textMan, resMan, system, psxDecoder, kVideoDecoderPSX);
#else
GUI::MessageDialog dialog(Common::String::format(_("PSX stream cutscene '%s' cannot be played in paletted mode"), filename.c_str()), _("OK"));
dialog.runModal();
return 0;
#endif
}
}
filename = Common::String::format("%s.smk", sequenceList[id]);
if (Common::File::exists(filename)) {
Video::SmackerDecoder *smkDecoder = new Video::SmackerDecoder();
return new MoviePlayer(vm, textMan, resMan, system, smkDecoder, kVideoDecoderSMK);
}
filename = Common::String::format("%s.dxa", sequenceList[id]);
if (Common::File::exists(filename)) {
#ifdef USE_ZLIB
Video::VideoDecoder *dxaDecoder = new Video::DXADecoder();
return new MoviePlayer(vm, textMan, resMan, system, dxaDecoder, kVideoDecoderDXA);
#else
GUI::MessageDialog dialog(_("DXA cutscenes found but ScummVM has been built without zlib"), _("OK"));
dialog.runModal();
return 0;
#endif
}
// Old MPEG2 cutscenes
filename = Common::String::format("%s.mp2", sequenceList[id]);
if (Common::File::exists(filename)) {
2012-11-29 00:34:09 +00:00
#ifdef USE_MPEG2
// HACK: Old ScummVM builds ignored the AVI frame rate field and forced the video
// to be played back at 12fps.
Video::VideoDecoder *aviDecoder = new Video::AVIDecoder(12);
return new MoviePlayer(vm, textMan, resMan, system, aviDecoder, kVideoDecoderMP2);
#else
GUI::MessageDialog dialog(_("MPEG-2 cutscenes found but ScummVM has been built without MPEG-2 support"), _("OK"));
dialog.runModal();
return 0;
2012-11-29 00:34:09 +00:00
#endif
}
if (!vm->isPsx() || scumm_stricmp(sequenceList[id], "enddemo") != 0) {
Common::String buf = Common::String::format(_("Cutscene '%s' not found"), sequenceList[id]);
GUI::MessageDialog dialog(buf, _("OK"));
dialog.runModal();
}
return 0;
}
} // End of namespace Sword1