scummvm/engines/twine/movies.cpp
Martin Gerhardy 68602a9e0a TWINE: little big adventure 2 fixes
implemented smk movieinfo handling
main menu shows correct text now
music track handling
2021-10-31 20:03:11 +01:00

543 lines
15 KiB
C++

/* 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 "twine/movies.h"
#include "common/endian.h"
#include "common/file.h"
#include "common/language.h"
#include "common/str.h"
#include "common/system.h"
#include "graphics/managed_surface.h"
#include "image/gif.h"
#include "twine/audio/music.h"
#include "twine/audio/sound.h"
#include "twine/input.h"
#include "twine/menu/interface.h"
#include "twine/renderer/screens.h"
#include "twine/resources/hqr.h"
#include "twine/resources/resources.h"
#include "twine/scene/grid.h"
#include "twine/shared.h"
#include "twine/twine.h"
#include "video/smk_decoder.h"
namespace TwinE {
/** FLA Frame Opcode types */
enum FlaFrameOpcode {
kLoadPalette = 1,
kFade = 2,
kPlaySample = 3,
kSampleBalance = 4,
kStopSample = 5,
kDeltaFrame = 6,
kBlackFrame = 7,
kKeyFrame = 8,
kCopy = 9,
kCopy2 = 16
};
/** FLA movie sample structure */
struct FLASampleStruct {
/** Number os samples */
int16 sampleNum = 0;
/** Sample frequency */
int16 freq = 0;
/** Numbers of time to repeat */
int16 repeat = 0;
uint8 balance = 0;
uint8 volumeLeft = 0;
uint8 volumeRight = 0;
};
/** FLA movie extension */
#define FLA_EXT ".fla"
void Movies::drawKeyFrame(Common::MemoryReadStream &stream, int32 width, int32 height) {
uint8 *destPtr = (uint8 *)_flaBuffer;
uint8 *startOfLine = destPtr;
for (int32 y = 0; y < height; ++y) {
const int8 lineEntryCount = stream.readByte();
for (int8 a = 0; a < lineEntryCount; a++) {
const int8 rleFlag = stream.readByte();
if (rleFlag < 0) {
const int8 rleCnt = ABS(rleFlag);
for (int8 b = 0; b < rleCnt; ++b) {
*destPtr++ = stream.readByte();
}
} else {
const char colorFill = stream.readByte();
Common::fill(&destPtr[0], &destPtr[rleFlag], colorFill);
destPtr += rleFlag;
}
}
startOfLine = destPtr = startOfLine + width;
}
}
void Movies::drawDeltaFrame(Common::MemoryReadStream &stream, int32 width) {
const uint16 skip = stream.readUint16LE() * width;
const int32 height = stream.readSint16LE();
uint8 *destPtr = (uint8 *)_flaBuffer + skip;
uint8 *startOfLine = destPtr;
for (int32 y = 0; y < height; ++y) {
const int8 lineEntryCount = stream.readByte();
for (int8 a = 0; a < lineEntryCount; ++a) {
destPtr += stream.readByte();
const int8 rleFlag = stream.readByte();
if (rleFlag > 0) {
for (int8 b = 0; b < rleFlag; ++b) {
*destPtr++ = stream.readByte();
}
} else {
const char colorFill = stream.readByte();
const int8 rleCnt = ABS(rleFlag);
Common::fill(&destPtr[0], &destPtr[rleCnt], colorFill);
destPtr += rleCnt;
}
}
startOfLine = destPtr = startOfLine + width;
}
}
void Movies::scaleFla2x() {
uint8 *source = (uint8 *)_flaBuffer;
uint8 *dest = (uint8 *)_engine->_imageBuffer.getPixels();
if (_engine->_cfgfile.Movie == CONF_MOVIE_FLAWIDE) {
Common::fill(&dest[0], &dest[_engine->_imageBuffer.w * 40], 0x00);
dest += _engine->_imageBuffer.w * 40;
}
for (int32 i = 0; i < FLASCREEN_HEIGHT; i++) {
for (int32 j = 0; j < FLASCREEN_WIDTH; j++) {
*dest++ = *source;
*dest++ = *source++;
}
if (_engine->_cfgfile.Movie == CONF_MOVIE_FLAWIDE) { // include wide bars
memcpy(dest, dest - _engine->_imageBuffer.w, FLASCREEN_WIDTH * 2);
dest += FLASCREEN_WIDTH * 2;
} else { // stretch the movie like original game.
if (i % 2) {
memcpy(dest, dest - _engine->_imageBuffer.w, FLASCREEN_WIDTH * 2);
dest += FLASCREEN_WIDTH * 2;
}
if (i % 10) {
memcpy(dest, dest - _engine->_imageBuffer.w, FLASCREEN_WIDTH * 2);
dest += FLASCREEN_WIDTH * 2;
}
}
}
if (_engine->_cfgfile.Movie == CONF_MOVIE_FLAWIDE) {
Common::fill(&dest[0], &dest[_engine->_imageBuffer.w * 40], 0x00);
}
}
void Movies::processFrame() {
FLASampleStruct sample;
_frameData.videoSize = _file.readSint16LE();
_frameData.frameVar0 = _file.readSint32LE();
if (_frameData.frameVar0 > _engine->_imageBuffer.w * _engine->_imageBuffer.h) {
warning("Skipping video frame - it would exceed the screen buffer: %i", _frameData.frameVar0);
return;
}
uint8 *outBuf = (uint8 *)_engine->_imageBuffer.getPixels();
_file.read(outBuf, _frameData.frameVar0);
if ((int32)_frameData.videoSize <= 0) {
return;
}
Common::MemoryReadStream stream(outBuf, _frameData.frameVar0);
for (int32 frame = 0; frame < _frameData.videoSize; ++frame) {
const uint16 opcode = stream.readUint16LE();
const uint16 opcodeBlockSize = stream.readUint16LE();
const int32 pos = stream.pos();
switch (opcode) {
case kLoadPalette: {
int16 numOfColor = stream.readSint16LE();
int16 startColor = stream.readSint16LE();
uint8 *dest = _engine->_screens->_palette + (startColor * 3);
stream.read(dest, numOfColor * 3);
break;
}
case kFade: {
int16 innerOpcpde = stream.readSint16LE();
switch (innerOpcpde) {
case 1: // fla flute
_engine->_music->playMidiMusic(26);
break;
case 2:
// FLA movies don't use cross fade
// fade out tricky
if (_fadeOut != 1) {
_engine->_screens->convertPalToRGBA(_engine->_screens->_palette, _engine->_screens->_paletteRGBACustom);
_engine->_screens->fadeToBlack(_engine->_screens->_paletteRGBACustom);
_fadeOut = 1;
}
break;
case 3:
_flaPaletteVar = true;
break;
case 4:
_engine->_music->stopMidiMusic();
break;
}
break;
}
case kPlaySample: {
sample.sampleNum = stream.readSint16LE();
sample.freq = stream.readSint16LE();
sample.repeat = stream.readSint16LE();
sample.balance = stream.readByte();
sample.volumeLeft = stream.readByte();
sample.volumeRight = stream.readByte();
_engine->_sound->playFlaSample(sample.sampleNum, sample.repeat, sample.balance, sample.volumeLeft, sample.volumeRight);
break;
}
case kStopSample: {
const int16 sampleNum = stream.readSint16LE();
if (sampleNum == -1) {
_engine->_sound->stopSamples();
} else {
_engine->_sound->stopSample(sampleNum);
}
break;
}
case kDeltaFrame: {
drawDeltaFrame(stream, FLASCREEN_WIDTH);
if (_fadeOut == 1) {
++_fadeOutFrames;
}
break;
}
case kKeyFrame: {
drawKeyFrame(stream, FLASCREEN_WIDTH, FLASCREEN_HEIGHT);
break;
}
case kBlackFrame: {
const Common::Rect rect(0, 0, 79, 199);
_engine->_interface->drawFilledRect(rect, 0);
break;
}
case kCopy:
case kCopy2: {
const Common::Rect rect(0, 0, 80, 200);
byte *ptr = (byte *)_engine->_frontVideoBuffer.getPixels();
for (int y = rect.top; y < rect.bottom; ++y) {
for (int x = rect.left; x < rect.right; ++x) {
*ptr++ = stream.readByte();
}
ptr = ptr + rect.width();
}
_engine->_frontVideoBuffer.addDirtyRect(rect);
break;
}
case kSampleBalance: {
/* int16 num = */ stream.readSint16LE();
/* uint8 offset = */ stream.readByte();
/* int16 balance = */ stream.readSint16LE();
/* uint8 volumeLeft = */ stream.readByte();
/* uint8 volumeRight = */ stream.readByte();
// TODO: change balance
break;
}
default: {
break;
}
}
stream.seek(pos + opcodeBlockSize);
}
}
Movies::Movies(TwinEEngine *engine) : _engine(engine) {}
void Movies::prepareGIF(int index) {
Image::GIFDecoder decoder;
Common::SeekableReadStream *stream = HQR::makeReadStream(Resources::HQR_FLAGIF_FILE, index);
if (stream == nullptr) {
warning("Failed to load gif hqr entry with id %i from %s", index, Resources::HQR_FLAGIF_FILE);
return;
}
if (!decoder.loadStream(*stream)) {
delete stream;
warning("Failed to load gif with id %i from %s", index, Resources::HQR_FLAGIF_FILE);
return;
}
const Graphics::Surface *surface = decoder.getSurface();
_engine->setPalette(0, decoder.getPaletteColorCount(), decoder.getPalette());
Graphics::ManagedSurface& target = _engine->_frontVideoBuffer;
const Common::Rect surfaceBounds(0, 0, surface->w, surface->h);
target.transBlitFrom(surface, surfaceBounds, target.getBounds(), 0, false, 0, 0xff, nullptr, true);
debug(2, "Show gif with id %i from %s", index, Resources::HQR_FLAGIF_FILE);
delete stream;
_engine->delaySkip(5000);
_engine->setPalette(_engine->_screens->_paletteRGBA);
}
void Movies::playGIFMovie(const char *flaName) {
if (!Common::File::exists(Resources::HQR_FLAGIF_FILE)) {
warning("%s file doesn't exist", Resources::HQR_FLAGIF_FILE);
return;
}
Common::String name(flaName);
name.toLowercase();
debug(1, "Play gif %s", name.c_str());
// TODO: use the HQR 23th entry (movies informations)
// TODO: there are gifs [1-18]
if (name == FLA_INTROD) {
prepareGIF(3);
prepareGIF(4);
prepareGIF(5);
} else if (name == "bateau" || name == "bateau2") {
prepareGIF(7);
} else if (name == "navette") {
prepareGIF(15);
} else if (name == "templebu") {
prepareGIF(12);
} else if (name == "flute2") {
prepareGIF(8); // TODO: same as glass2?
} else if (name == "glass2") {
prepareGIF(8); // TODO: same as flute2?
} else if (name == "surf") {
prepareGIF(9);
} else if (name == "verser" || name == "verser2") {
prepareGIF(10);
} else if (name == "neige2") {
prepareGIF(11);
} else if (name == "capture") {
prepareGIF(14); // TODO: same as sendel?
} else if (name == "sendel") {
prepareGIF(14); // TODO: same as capture?
} else if (name == "sendel2") {
prepareGIF(17);
} else if (name == FLA_DRAGON3) {
prepareGIF(1);
prepareGIF(2);
} else if (name == "baffe" || name.matchString("baffe#")) {
prepareGIF(6);
} else {
warning("unknown gif image: %s", name.c_str());
}
}
bool Movies::playMovie(const char *name) {
if (_engine->isLBA2()) {
const int index = _engine->_resources->findSmkMovieIndex(name);
return playSmkMovie(name, index);
}
_engine->_sound->stopSamples();
Common::String fileNamePath = name;
const size_t n = fileNamePath.findLastOf(".");
if (n != Common::String::npos) {
fileNamePath.erase(n);
}
if (_engine->_cfgfile.Movie == CONF_MOVIE_FLAGIF) {
playGIFMovie(fileNamePath.c_str());
return true;
}
_engine->_music->stopMusic();
_fadeOut = -1;
_fadeOutFrames = 0;
_file.close();
if (!_file.open(fileNamePath + FLA_EXT)) {
warning("Failed to open fla movie '%s'", fileNamePath.c_str());
playGIFMovie(fileNamePath.c_str());
return true;
}
const uint32 version = _file.readUint32LE();
_file.skip(2);
_flaHeaderData.numOfFrames = _file.readUint32LE();
_flaHeaderData.speed = _file.readByte();
_flaHeaderData.var1 = _file.readByte();
debug(2, "Unknown byte in fla file: %i", _flaHeaderData.var1);
_flaHeaderData.xsize = _file.readUint16LE();
_flaHeaderData.ysize = _file.readUint16LE();
_samplesInFla = (int16)_file.readUint16LE();
const uint16 unk2 = _file.readUint16LE();
debug(2, "Unknown uint16 in fla file: %i", unk2);
_file.skip(4 * _samplesInFla);
bool finished = false;
if (version != MKTAG('V', '1', '.', '3')) {
int32 currentFrame = 0;
debug("Play fla: %s", name);
ScopedKeyMap scopedKeyMap(_engine, cutsceneKeyMapId);
_flaPaletteVar = true;
do {
FrameMarker frame(_engine, _flaHeaderData.speed);
_engine->readKeys();
if (_engine->shouldQuit()) {
break;
}
if (currentFrame == _flaHeaderData.numOfFrames) {
finished = true;
break;
}
processFrame();
scaleFla2x();
_engine->_frontVideoBuffer.blitFrom(_engine->_imageBuffer, _engine->_imageBuffer.getBounds(), _engine->_frontVideoBuffer.getBounds());
// Only blit to screen if isn't a fade
if (_fadeOut == -1) {
_engine->_screens->convertPalToRGBA(_engine->_screens->_palette, _engine->_screens->_paletteRGBACustom);
if (currentFrame == 0) {
// fade in the first frame
_engine->_screens->fadeIn(_engine->_screens->_paletteRGBACustom);
} else {
_engine->setPalette(_engine->_screens->_paletteRGBACustom);
}
}
// TRICKY: fade in tricky
if (_fadeOutFrames >= 2) {
_engine->_screens->convertPalToRGBA(_engine->_screens->_palette, _engine->_screens->_paletteRGBACustom);
_engine->_screens->fadeToPal(_engine->_screens->_paletteRGBACustom);
_fadeOut = -1;
_fadeOutFrames = 0;
}
currentFrame++;
} while (!_engine->_input->toggleAbortAction());
}
if (_engine->_cfgfile.CrossFade) {
_engine->crossFade(_engine->_screens->_paletteRGBACustom);
} else {
_engine->_screens->fadeToBlack(_engine->_screens->_paletteRGBACustom);
}
_engine->_sound->stopSamples();
return finished;
}
class TwineSmackerDecoder : public Video::SmackerDecoder {
public:
void enableLanguage(int track, int volume) {
AudioTrack* audio = getAudioTrack(track);
if (audio == nullptr) {
return;
}
audio->setMute(false);
audio->setVolume(CLIP<int>(volume, 0, Audio::Mixer::kMaxMixerVolume));
}
};
bool Movies::playSmkMovie(const char *name, int index) {
assert(_engine->isLBA2());
TwineSmackerDecoder decoder;
Common::SeekableReadStream *stream = HQR::makeReadStream(TwineResource(Resources::HQR_VIDEO_FILE, index));
if (stream == nullptr) {
warning("Failed to find smacker video %i", index);
return false;
}
if (!decoder.loadStream(stream)) {
warning("Failed to load smacker video %i", index);
return false;
}
const int volume = _engine->_system->getMixer()->getVolumeForSoundType(Audio::Mixer::kMusicSoundType);
decoder.setVolume(CLIP<int>(volume, 0, Audio::Mixer::kMaxMixerVolume));
decoder.start();
decoder.setAudioTrack(0); // music
if (_engine->_cfgfile.Voice) {
int additionalAudioTrack = -1;
if (!scumm_strnicmp(name, "INTRO", 5)) {
switch (_engine->getGameLang()) {
default:
case Common::Language::EN_ANY:
case Common::Language::EN_GRB:
case Common::Language::EN_USA:
additionalAudioTrack = 3;
break;
case Common::Language::DE_DEU:
additionalAudioTrack = 2;
break;
case Common::Language::FR_FRA:
additionalAudioTrack = 1;
break;
}
}
const int speechVolume = _engine->_system->getMixer()->getVolumeForSoundType(Audio::Mixer::kSpeechSoundType);
debug(3, "Play additional speech track: %i (of %i tracks)", additionalAudioTrack, decoder.getAudioTrackCount());
decoder.enableLanguage(additionalAudioTrack, speechVolume);
} else {
debug(3, "Disabled smacker speech");
}
for (;;) {
if (decoder.endOfVideo()) {
break;
}
FrameMarker frame(_engine);
_engine->_input->readKeys();
if (_engine->shouldQuit() || _engine->_input->toggleAbortAction()) {
break;
}
if (decoder.needsUpdate()) {
const Graphics::Surface *frameSurf = decoder.decodeNextFrame();
if (!frameSurf) {
continue;
}
if (decoder.hasDirtyPalette()) {
_engine->setPalette(0, 256, decoder.getPalette());
}
Graphics::ManagedSurface& target = _engine->_frontVideoBuffer;
const Common::Rect frameBounds(0, 0, frameSurf->w, frameSurf->h);
target.transBlitFrom(*frameSurf, frameBounds, target.getBounds(), 0, false, 0, 0xff, nullptr, true);
}
}
decoder.close();
return true;
}
} // namespace TwinE