504 lines
16 KiB
C++
504 lines
16 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 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 <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
#include "mediastation/actors/movie.h"
|
|
#include "mediastation/debugchannels.h"
|
|
#include "mediastation/mediastation.h"
|
|
|
|
namespace MediaStation {
|
|
|
|
MovieFrameHeader::MovieFrameHeader(Chunk &chunk) : BitmapHeader(chunk) {
|
|
_index = chunk.readTypedUint32();
|
|
debugC(5, kDebugLoading, "MovieFrameHeader::MovieFrameHeader(): _index = 0x%x (@0x%llx)", _index, static_cast<long long int>(chunk.pos()));
|
|
_keyframeEndInMilliseconds = chunk.readTypedUint32();
|
|
}
|
|
|
|
MovieFrame::MovieFrame(Chunk &chunk) {
|
|
if (g_engine->isFirstGenerationEngine()) {
|
|
blitType = static_cast<MovieBlitType>(chunk.readTypedUint16());
|
|
startInMilliseconds = chunk.readTypedUint32();
|
|
endInMilliseconds = chunk.readTypedUint32();
|
|
// These are unsigned in the data files but ScummVM expects signed.
|
|
leftTop.x = static_cast<int16>(chunk.readTypedUint16());
|
|
leftTop.y = static_cast<int16>(chunk.readTypedUint16());
|
|
index = chunk.readTypedUint32();
|
|
keyframeIndex = chunk.readTypedUint32();
|
|
keepAfterEnd = chunk.readTypedByte();
|
|
} else {
|
|
layerId = chunk.readTypedUint32();
|
|
blitType = static_cast<MovieBlitType>(chunk.readTypedUint16());
|
|
startInMilliseconds = chunk.readTypedUint32();
|
|
endInMilliseconds = chunk.readTypedUint32();
|
|
// These are unsigned in the data files but ScummVM expects signed.
|
|
leftTop.x = static_cast<int16>(chunk.readTypedUint16());
|
|
leftTop.y = static_cast<int16>(chunk.readTypedUint16());
|
|
zIndex = chunk.readTypedSint16();
|
|
// This represents the difference between the left-top coordinate of the
|
|
// keyframe (if applicable) and the left coordinate of this frame. Zero
|
|
// if there is no keyframe.
|
|
diffBetweenKeyframeAndFrame.x = chunk.readTypedSint16();
|
|
diffBetweenKeyframeAndFrame.y = chunk.readTypedSint16();
|
|
index = chunk.readTypedUint32();
|
|
keyframeIndex = chunk.readTypedUint32();
|
|
keepAfterEnd = chunk.readTypedByte();
|
|
debugC(5, kDebugLoading, "MovieFrame::MovieFrame(): _blitType = %d, _startInMilliseconds = %d, \
|
|
_endInMilliseconds = %d, _left = %d, _top = %d, _zIndex = %d, _diffBetweenKeyframeAndFrameX = %d, \
|
|
_diffBetweenKeyframeAndFrameY = %d, _index = %d, _keyframeIndex = %d, _keepAfterEnd = %d (@0x%llx)",
|
|
blitType, startInMilliseconds, endInMilliseconds, leftTop.x, leftTop.y, zIndex, diffBetweenKeyframeAndFrame.x, \
|
|
diffBetweenKeyframeAndFrame.y, index, keyframeIndex, keepAfterEnd, static_cast<long long int>(chunk.pos()));
|
|
}
|
|
}
|
|
|
|
MovieFrameImage::MovieFrameImage(Chunk &chunk, MovieFrameHeader *header) : Bitmap(chunk, header) {
|
|
_bitmapHeader = header;
|
|
}
|
|
|
|
MovieFrameImage::~MovieFrameImage() {
|
|
// The base class destructor takes care of deleting the bitmap header, so
|
|
// we don't need to delete that here.
|
|
}
|
|
|
|
StreamMovieActor::~StreamMovieActor() {
|
|
unregisterWithStreamManager();
|
|
if (_streamFeed != nullptr) {
|
|
g_engine->getStreamFeedManager()->closeStreamFeed(_streamFeed);
|
|
_streamFeed = nullptr;
|
|
}
|
|
|
|
delete _streamFrames;
|
|
_streamFrames = nullptr;
|
|
|
|
delete _streamSound;
|
|
_streamSound = nullptr;
|
|
}
|
|
|
|
void StreamMovieActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
|
|
switch (paramType) {
|
|
case kActorHeaderActorId: {
|
|
// We already have this actor's ID, so we will just verify it is the same
|
|
// as the ID we have already read.
|
|
uint32 duplicateActorId = chunk.readTypedUint16();
|
|
if (duplicateActorId != _id) {
|
|
warning("%s: Duplicate actor ID %d does not match original ID %d", __func__, duplicateActorId, _id);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case kActorHeaderMovieLoadType:
|
|
_loadType = chunk.readTypedByte();
|
|
break;
|
|
|
|
case kActorHeaderChannelIdent:
|
|
_channelIdent = chunk.readTypedChannelIdent();
|
|
registerWithStreamManager();
|
|
break;
|
|
|
|
case kActorHeaderHasOwnSubfile: {
|
|
bool hasOwnSubfile = static_cast<bool>(chunk.readTypedByte());
|
|
if (!hasOwnSubfile) {
|
|
error("%s: StreamMovieActor doesn't have a subfile", __func__);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case kActorHeaderStartup:
|
|
_isVisible = static_cast<bool>(chunk.readTypedByte());
|
|
break;
|
|
|
|
case kActorHeaderMovieAudioChannelIdent: {
|
|
ChannelIdent soundChannelIdent = chunk.readTypedChannelIdent();
|
|
_streamSound->setChannelIdent(soundChannelIdent);
|
|
_streamSound->registerWithStreamManager();
|
|
break;
|
|
}
|
|
|
|
case kActorHeaderMovieAnimationChannelIdent: {
|
|
ChannelIdent framesChannelIdent = chunk.readTypedChannelIdent();
|
|
_streamFrames->setChannelIdent(framesChannelIdent);
|
|
_streamFrames->registerWithStreamManager();
|
|
break;
|
|
}
|
|
|
|
case kActorHeaderSoundInfo:
|
|
_streamSound->_audioSequence.readParameters(chunk);
|
|
break;
|
|
|
|
default:
|
|
SpatialEntity::readParameter(chunk, paramType);
|
|
}
|
|
}
|
|
|
|
ScriptValue StreamMovieActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) {
|
|
ScriptValue returnValue;
|
|
|
|
switch (methodId) {
|
|
case kTimePlayMethod: {
|
|
assert(args.empty());
|
|
timePlay();
|
|
return returnValue;
|
|
}
|
|
|
|
case kSpatialShowMethod: {
|
|
assert(args.empty());
|
|
setVisibility(true);
|
|
updateFrameState();
|
|
return returnValue;
|
|
}
|
|
|
|
case kTimeStopMethod: {
|
|
assert(args.empty());
|
|
timeStop();
|
|
return returnValue;
|
|
}
|
|
|
|
case kSpatialHideMethod: {
|
|
assert(args.empty());
|
|
setVisibility(false);
|
|
return returnValue;
|
|
}
|
|
|
|
case kIsPlayingMethod: {
|
|
assert(args.empty());
|
|
returnValue.setToBool(_isPlaying);
|
|
return returnValue;
|
|
}
|
|
|
|
case kGetLeftXMethod: {
|
|
assert(args.empty());
|
|
double left = static_cast<double>(_boundingBox.left);
|
|
returnValue.setToFloat(left);
|
|
return returnValue;
|
|
}
|
|
|
|
case kGetTopYMethod: {
|
|
assert(args.empty());
|
|
double top = static_cast<double>(_boundingBox.top);
|
|
returnValue.setToFloat(top);
|
|
return returnValue;
|
|
}
|
|
|
|
default:
|
|
return SpatialEntity::callMethod(methodId, args);
|
|
}
|
|
}
|
|
|
|
void StreamMovieActor::timePlay() {
|
|
if (_streamFeed == nullptr) {
|
|
_streamFeed = g_engine->getStreamFeedManager()->openStreamFeed(_id);
|
|
_streamFeed->readData();
|
|
}
|
|
|
|
if (_isPlaying) {
|
|
return;
|
|
}
|
|
|
|
_streamSound->_audioSequence.play();
|
|
_framesNotYetShown = _streamFrames->_frames;
|
|
_framesOnScreen.clear();
|
|
_isPlaying = true;
|
|
_startTime = g_system->getMillis();
|
|
_lastProcessedTime = 0;
|
|
runEventHandlerIfExists(kMovieBeginEvent);
|
|
process();
|
|
}
|
|
|
|
void StreamMovieActor::timeStop() {
|
|
if (!_isPlaying) {
|
|
return;
|
|
}
|
|
|
|
for (MovieFrame *frame : _framesOnScreen) {
|
|
invalidateRect(getFrameBoundingBox(frame));
|
|
}
|
|
_streamSound->_audioSequence.stop();
|
|
_framesNotYetShown.empty();
|
|
if (_hasStill) {
|
|
_framesNotYetShown = _streamFrames->_frames;
|
|
}
|
|
_framesOnScreen.clear();
|
|
_startTime = 0;
|
|
_lastProcessedTime = 0;
|
|
_isPlaying = false;
|
|
runEventHandlerIfExists(kMovieStoppedEvent);
|
|
}
|
|
|
|
void StreamMovieActor::process() {
|
|
if (_isVisible) {
|
|
if (_isPlaying) {
|
|
processTimeEventHandlers();
|
|
}
|
|
updateFrameState();
|
|
}
|
|
}
|
|
|
|
void StreamMovieActor::setVisibility(bool visibility) {
|
|
if (visibility != _isVisible) {
|
|
_isVisible = visibility;
|
|
invalidateLocalBounds();
|
|
}
|
|
}
|
|
|
|
void StreamMovieActor::updateFrameState() {
|
|
uint movieTime = 0;
|
|
if (_isPlaying) {
|
|
uint currentTime = g_system->getMillis();
|
|
movieTime = currentTime - _startTime;
|
|
}
|
|
debugC(5, kDebugGraphics, "StreamMovieActor::updateFrameState (%d): Starting update (movie time: %d)", _id, movieTime);
|
|
|
|
// This complexity is necessary becuase movies can have more than one frame
|
|
// showing at the same time - for instance, a movie background and an
|
|
// animation on that background are a part of the saem movie and are on
|
|
// screen at the same time, it's just the starting and ending times of one
|
|
// can be different from the starting and ending times of another.
|
|
//
|
|
// We can rely on the frames being ordered in order of their start. First,
|
|
// see if there are any new frames to show.
|
|
for (auto it = _framesNotYetShown.begin(); it != _framesNotYetShown.end();) {
|
|
MovieFrame *frame = *it;
|
|
bool isAfterStart = movieTime >= frame->startInMilliseconds;
|
|
if (isAfterStart) {
|
|
_framesOnScreen.insert(frame);
|
|
invalidateRect(getFrameBoundingBox(frame));
|
|
|
|
// We don't need ++it because we will either have another frame
|
|
// that needs to be drawn, or we have reached the end of the new
|
|
// frames.
|
|
it = _framesNotYetShown.erase(it);
|
|
} else {
|
|
// We've hit a frame that shouldn't yet be shown.
|
|
// Rely on the ordering to not bother with any further frames.
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Now see if there are any old frames that no longer need to be shown.
|
|
for (auto it = _framesOnScreen.begin(); it != _framesOnScreen.end();) {
|
|
MovieFrame *frame = *it;
|
|
bool isAfterEnd = movieTime >= frame->endInMilliseconds;
|
|
if (isAfterEnd) {
|
|
invalidateRect(getFrameBoundingBox(frame));
|
|
it = _framesOnScreen.erase(it);
|
|
|
|
if (_framesOnScreen.empty() && movieTime >= _fullTime) {
|
|
_isPlaying = false;
|
|
if (_hasStill) {
|
|
_framesNotYetShown = _streamFrames->_frames;
|
|
updateFrameState();
|
|
}
|
|
runEventHandlerIfExists(kMovieEndEvent);
|
|
break;
|
|
}
|
|
} else {
|
|
++it;
|
|
}
|
|
}
|
|
|
|
// Show the frames that are currently active, for debugging purposes.
|
|
for (MovieFrame *frame : _framesOnScreen) {
|
|
debugC(5, kDebugGraphics, " (time: %d ms) Frame %d (%d x %d) @ (%d, %d); start: %d ms, end: %d ms, zIndex = %d", \
|
|
movieTime, frame->index, frame->image->width(), frame->image->height(), frame->leftTop.x, frame->leftTop.y, frame->startInMilliseconds, frame->endInMilliseconds, frame->zIndex);
|
|
}
|
|
}
|
|
|
|
void StreamMovieActor::draw(DisplayContext &displayContext) {
|
|
for (MovieFrame *frame : _framesOnScreen) {
|
|
Common::Rect bbox = getFrameBoundingBox(frame);
|
|
|
|
switch (frame->blitType) {
|
|
case kUncompressedMovieBlit:
|
|
g_engine->getDisplayManager()->imageBlit(bbox.origin(), frame->image, _dissolveFactor, &displayContext);
|
|
break;
|
|
|
|
case kUncompressedDeltaMovieBlit:
|
|
g_engine->getDisplayManager()->imageDeltaBlit(
|
|
bbox.origin(), frame->diffBetweenKeyframeAndFrame,
|
|
frame->image, frame->keyframeImage, _dissolveFactor, &displayContext);
|
|
break;
|
|
|
|
case kCompressedDeltaMovieBlit:
|
|
if (frame->keyframeImage->isCompressed()) {
|
|
decompressIntoAuxImage(frame);
|
|
}
|
|
g_engine->getDisplayManager()->imageDeltaBlit(
|
|
bbox.origin(), frame->diffBetweenKeyframeAndFrame,
|
|
frame->image, frame->keyframeImage, _dissolveFactor, &displayContext);
|
|
break;
|
|
|
|
default:
|
|
error("%s: Got unknown movie frame blit type: %d", __func__, frame->blitType);
|
|
}
|
|
}
|
|
}
|
|
|
|
Common::Rect StreamMovieActor::getFrameBoundingBox(MovieFrame *frame) {
|
|
// Use _boundingBox directly (which may be temporarily offset by camera rendering)
|
|
// The camera offset is already applied to _boundingBox by pushBoundingBoxOffset()
|
|
Common::Point origin = _boundingBox.origin() + frame->leftTop;
|
|
Common::Rect bbox = Common::Rect(origin, frame->image->width(), frame->image->height());
|
|
return bbox;
|
|
}
|
|
|
|
StreamMovieActorFrames::~StreamMovieActorFrames() {
|
|
unregisterWithStreamManager();
|
|
|
|
for (MovieFrame *frame : _frames) {
|
|
delete frame;
|
|
}
|
|
_frames.clear();
|
|
|
|
for (MovieFrameImage *image : _images) {
|
|
delete image;
|
|
}
|
|
_images.clear();
|
|
}
|
|
|
|
void StreamMovieActorFrames::readChunk(Chunk &chunk) {
|
|
uint sectionType = chunk.readTypedUint16();
|
|
switch ((MovieSectionType)sectionType) {
|
|
case kMovieImageDataSection:
|
|
readImageData(chunk);
|
|
break;
|
|
|
|
case kMovieFrameDataSection:
|
|
readFrameData(chunk);
|
|
break;
|
|
|
|
default:
|
|
error("%s: Unknown movie still section type", __func__);
|
|
}
|
|
|
|
for (MovieFrame *frame : _frames) {
|
|
if (frame->endInMilliseconds > _parent->_fullTime) {
|
|
_parent->_fullTime = frame->endInMilliseconds;
|
|
}
|
|
if (frame->keepAfterEnd) {
|
|
_parent->_hasStill = true;
|
|
}
|
|
}
|
|
|
|
if (_parent->_hasStill) {
|
|
_parent->_framesNotYetShown = _frames;
|
|
}
|
|
}
|
|
|
|
StreamMovieActorSound::~StreamMovieActorSound() {
|
|
unregisterWithStreamManager();
|
|
}
|
|
|
|
void StreamMovieActorSound::readChunk(Chunk &chunk) {
|
|
_audioSequence.readChunk(chunk);
|
|
}
|
|
|
|
StreamMovieActor::StreamMovieActor() : _framesOnScreen(StreamMovieActor::compareFramesByZIndex), SpatialEntity(kActorTypeMovie) {
|
|
_streamFrames = new StreamMovieActorFrames(this);
|
|
_streamSound = new StreamMovieActorSound();
|
|
}
|
|
|
|
void StreamMovieActor::readChunk(Chunk &chunk) {
|
|
MovieSectionType sectionType = static_cast<MovieSectionType>(chunk.readTypedUint16());
|
|
if (sectionType == kMovieRootSection) {
|
|
parseMovieHeader(chunk);
|
|
} else if (sectionType == kMovieChunkMarkerSection) {
|
|
parseMovieChunkMarker(chunk);
|
|
} else {
|
|
error("%s: Got unused movie chunk header section", __func__);
|
|
}
|
|
}
|
|
|
|
void StreamMovieActor::parseMovieHeader(Chunk &chunk) {
|
|
_chunkCount = chunk.readTypedUint16();
|
|
_frameRate = chunk.readTypedDouble();
|
|
debugC(5, kDebugLoading, "%s: chunkCount = 0x%x, frameRate = %f (@0x%llx)", __func__, _chunkCount, _frameRate, static_cast<long long int>(chunk.pos()));
|
|
|
|
Common::Array<uint> chunkLengths;
|
|
for (uint i = 0; i < _chunkCount; i++) {
|
|
uint chunkLength = chunk.readTypedUint32();
|
|
debugC(5, kDebugLoading, "StreamMovieActor::readSubfile(): chunkLength = 0x%x (@0x%llx)", chunkLength, static_cast<long long int>(chunk.pos()));
|
|
chunkLengths.push_back(chunkLength);
|
|
}
|
|
}
|
|
|
|
void StreamMovieActor::parseMovieChunkMarker(Chunk &chunk) {
|
|
// TODO: There is no warning here because that would spam with thousands of warnings.
|
|
// This takes care of scheduling stream load and such - it doesn't actually read from the
|
|
// chunk that is passed in. Since we don't need that scheduling since we are currently reading
|
|
// the whole movie at once rather than streaming it from the CD-ROM, we don't currently need
|
|
// to do much here anyway.
|
|
}
|
|
|
|
void StreamMovieActor::invalidateRect(const Common::Rect &rect) {
|
|
invalidateLocalBounds();
|
|
}
|
|
|
|
void StreamMovieActor::decompressIntoAuxImage(MovieFrame *frame) {
|
|
const Common::Point origin(0, 0);
|
|
frame->keyframeImage->_image.create(frame->keyframeImage->width(), frame->keyframeImage->height(), Graphics::PixelFormat::createFormatCLUT8());
|
|
frame->keyframeImage->_image.setTransparentColor(0);
|
|
g_engine->getDisplayManager()->imageBlit(origin, frame->keyframeImage, 1.0, nullptr, &frame->keyframeImage->_image);
|
|
}
|
|
|
|
void StreamMovieActorFrames::readImageData(Chunk &chunk) {
|
|
MovieFrameHeader *header = new MovieFrameHeader(chunk);
|
|
MovieFrameImage *frame = new MovieFrameImage(chunk, header);
|
|
_images.push_back(frame);
|
|
}
|
|
|
|
void StreamMovieActorFrames::readFrameData(Chunk &chunk) {
|
|
uint frameDataToRead = chunk.readTypedUint16();
|
|
for (uint i = 0; i < frameDataToRead; i++) {
|
|
MovieFrame *frame = new MovieFrame(chunk);
|
|
|
|
// We cannot use a hashmap here because multiple frames can have the
|
|
// same index, and frames are not necessarily in index order. So we'll
|
|
// do a linear search, which is how the original does it.
|
|
for (MovieFrameImage *image : _images) {
|
|
if (image->index() == frame->index) {
|
|
frame->image = image;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (frame->keyframeIndex != 0) {
|
|
for (MovieFrameImage *image : _images) {
|
|
if (image->index() == frame->keyframeIndex) {
|
|
frame->keyframeImage = image;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
_frames.push_back(frame);
|
|
}
|
|
}
|
|
|
|
int StreamMovieActor::compareFramesByZIndex(const MovieFrame *a, const MovieFrame *b) {
|
|
if (b->zIndex > a->zIndex) {
|
|
return 1;
|
|
} else if (a->zIndex > b->zIndex) {
|
|
return -1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
} // End of namespace MediaStation
|