1478 lines
44 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 "common/config-manager.h"
#include "common/file.h"
#include "common/memstream.h"
#include "common/substream.h"
#include "audio/audiostream.h"
#include "graphics/macgui/mactext.h"
#ifdef USE_PNG
#include "image/png.h"
#endif
#include "director/director.h"
#include "director/cast.h"
#include "director/castmember.h"
#include "director/score.h"
#include "director/frame.h"
#include "director/movie.h"
#include "director/sound.h"
#include "director/cursor.h"
#include "director/channel.h"
#include "director/sprite.h"
#include "director/window.h"
#include "director/util.h"
#include "director/lingo/lingo.h"
namespace Director {
#include "director/palette-fade.h"
Score::Score(Movie *movie) {
_movie = movie;
_window = movie->getWindow();
_vm = _movie->getVM();
_lingo = _vm->getLingo();
_soundManager = _window->getSoundManager();
_puppetTempo = 0x00;
_puppetPalette = false;
_lastPalette = 0;
_paletteTransitionIndex = 0;
memset(_paletteSnapshotBuffer, 0, 768);
_labels = nullptr;
_currentFrameRate = 20;
_currentFrame = 0;
_nextFrame = 0;
_currentLabel = 0;
_nextFrameTime = 0;
_lastTempo = 0;
_waitForChannel = 0;
_waitForVideoChannel = 0;
_cursorDirty = false;
_waitForClick = false;
_waitForClickCursor = false;
_activeFade = 0;
_playState = kPlayNotStarted;
_numChannelsDisplayed = 0;
}
Score::~Score() {
for (uint i = 0; i < _frames.size(); i++)
delete _frames[i];
for (uint i = 0; i < _channels.size(); i++)
delete _channels[i];
if (_labels)
for (Common::SortedArray<Label *>::iterator it = _labels->begin(); it != _labels->end(); ++it)
delete *it;
delete _labels;
}
int Score::getCurrentPalette() {
return _lastPalette;
}
int Score::resolvePaletteId(int id) {
// TODO: Palette ID should be a CastMemberID to allow for palettes in different casts
// 255 represent system palette in D2
if (id == 255) {
id = g_director->getCurrentMovie()->getCast()->_defaultPalette;
} else if (id > 0) {
CastMember *member = _movie->getCastMember(CastMemberID(id, 0));
id = (member && member->_type == kCastPalette) ? ((PaletteCastMember *)member)->getPaletteId() : 0;
}
return id;
}
bool Score::processImmediateFrameScript(Common::String s, int id) {
s.trim();
// In D2/D3 this specifies immediately the sprite/field properties
if (!s.compareToIgnoreCase("moveableSprite") || !s.compareToIgnoreCase("editableText")) {
_immediateActions[id] = true;
}
return false;
}
bool Score::processFrozenScripts() {
// Unfreeze any in-progress scripts and attempt to run them
// to completion.
while (uint32 count = _window->frozenLingoStateCount()) {
_window->thawLingoState();
g_lingo->switchStateFromWindow();
g_lingo->execute();
if (_window->frozenLingoStateCount() >= count) {
debugC(3, kDebugLingoExec, "Score::processFrozenScripts(): State froze again mid-thaw, interrupting");
return false;
}
}
return true;
}
uint16 Score::getLabel(Common::String &label) {
if (!_labels) {
warning("Score::getLabel: No labels set");
return 0;
}
for (Common::SortedArray<Label *>::iterator i = _labels->begin(); i != _labels->end(); ++i) {
if ((*i)->name.equalsIgnoreCase(label)) {
return (*i)->number;
}
}
return 0;
}
Common::String *Score::getLabelList() {
Common::String *res = new Common::String;
for (Common::SortedArray<Label *>::iterator i = _labels->begin(); i != _labels->end(); ++i) {
*res += (*i)->name;
*res += '\n';
}
return res;
}
Common::String *Score::getFrameLabel(uint id) {
for (Common::SortedArray<Label *>::iterator i = _labels->begin(); i != _labels->end(); ++i) {
if ((*i)->number == id) {
return new Common::String((*i)->name);
break;
}
}
return new Common::String;
}
void Score::setStartToLabel(Common::String &label) {
uint16 num = getLabel(label);
if (num == 0)
warning("Label %s not found", label.c_str());
else
_nextFrame = num;
}
void Score::gotoLoop() {
// This command has the playback head continuously return to the first marker to the left and then loop back.
// If no marker are to the left of the playback head, the playback head continues to the right.
if (_labels == nullptr) {
_nextFrame = 1;
return;
} else {
_nextFrame = _currentLabel;
}
_vm->_skipFrameAdvance = true;
}
int Score::getCurrentLabelNumber() {
Common::SortedArray<Label *>::iterator i;
if (!_labels)
return 0;
int frame = 0;
for (i = _labels->begin(); i != _labels->end(); ++i) {
if ((*i)->number <= _currentFrame)
frame = (*i)->number;
}
return frame;
}
void Score::gotoNext() {
// we can just try to use the current frame and get the next label
_nextFrame = getNextLabelNumber(_currentFrame);
}
void Score::gotoPrevious() {
// we actually need the frame of the label prior to the most recent label.
_nextFrame = getPreviousLabelNumber(getCurrentLabelNumber());
}
int Score::getNextLabelNumber(int referenceFrame) {
if (_labels == nullptr || _labels->size() == 0)
return 0;
Common::SortedArray<Label *>::iterator i;
for (i = _labels->begin(); i != _labels->end(); ++i) {
if ((*i)->number >= referenceFrame) {
int n = (*i)->number;
++i;
if (i != _labels->end()) {
// return to the first marker to to the right
return (*i)->number;
} else {
// if no markers are to the right of the playback head,
// the playback head goes to the first marker to the left
return n;
}
}
}
// If there are not markers to the left,
// the playback head goes to frame 1, (Director frame array start from 1, engine from 0)
return 0;
}
int Score::getPreviousLabelNumber(int referenceFrame) {
if (_labels == nullptr || _labels->size() == 0)
return 0;
// One label
if (_labels->begin() == _labels->end())
return (*_labels->begin())->number;
Common::SortedArray<Label *>::iterator previous = _labels->begin();
Common::SortedArray<Label *>::iterator i;
for (i = (previous + 1); i != _labels->end(); ++i, ++previous) {
if ((*i)->number >= referenceFrame)
return (*previous)->number;
}
return 0;
}
void Score::startPlay() {
_currentFrame = 1;
_playState = kPlayStarted;
_nextFrameTime = 0;
if (_frames.size() <= 1) { // We added one empty sprite
warning("Score::startLoop(): Movie has no frames");
_playState = kPlayStopped;
return;
}
_lastPalette = _frames[_currentFrame]->_palette.paletteId;
if (!_lastPalette)
_lastPalette = _movie->getCast()->_defaultPalette;
_vm->setPalette(resolvePaletteId(_lastPalette));
// All frames in the same movie have the same number of channels
if (_playState != kPlayStopped)
for (uint i = 0; i < _frames[1]->_sprites.size(); i++)
_channels.push_back(new Channel(_frames[1]->_sprites[i], i));
if (_vm->getVersion() >= 300)
_movie->processEvent(kEventStartMovie);
}
void Score::step() {
if (_playState == kPlayStopped)
return;
if (!_movie->_userEventQueue.empty()) {
_lingo->processEvents(_movie->_userEventQueue);
} else if (_vm->getVersion() >= 300 && !_window->_newMovieStarted && _playState != kPlayStopped) {
_movie->processEvent(kEventIdle);
}
update();
if (debugChannelSet(-1, kDebugFewFramesOnly) || debugChannelSet(-1, kDebugScreenshot)) {
warning("Score::startLoop(): ran frame %0d", g_director->_framesRan);
g_director->_framesRan++;
}
if (debugChannelSet(-1, kDebugFewFramesOnly) && g_director->_framesRan > kFewFamesMaxCounter) {
warning("Score::startLoop(): exiting due to debug few frames only");
_playState = kPlayStopped;
return;
}
if (debugChannelSet(-1, kDebugScreenshot))
screenShot();
}
void Score::stopPlay() {
if (_vm->getVersion() >= 300)
_movie->processEvent(kEventStopMovie);
_lingo->executePerFrameHook(-1, 0);
}
void Score::update() {
if (_activeFade) {
if (!_soundManager->fadeChannel(_activeFade))
_activeFade = 0;
}
if (!debugChannelSet(-1, kDebugFast)) {
bool keepWaiting = false;
debugC(8, kDebugLoading, "Score::update(): nextFrameTime: %d, time: %d", _nextFrameTime, g_system->getMillis(false));
if (_waitForChannel) {
if (_soundManager->isChannelActive(_waitForChannel)) {
keepWaiting = true;
} else {
_waitForChannel = 0;
}
} else if (_waitForClick) {
if (g_system->getMillis() >= _nextFrameTime + 1000) {
_waitForClickCursor = !_waitForClickCursor;
renderCursor(_movie->getWindow()->getMousePos());
_nextFrameTime = g_system->getMillis();
}
keepWaiting = true;
} else if (_waitForVideoChannel) {
Channel *movieChannel = _channels[_waitForVideoChannel];
if (movieChannel->isActiveVideo() && movieChannel->_movieRate != 0.0) {
keepWaiting = true;
} else {
_waitForVideoChannel = 0;
}
} else if (g_system->getMillis() < _nextFrameTime && !_nextFrame) {
keepWaiting = true;
}
if (keepWaiting) {
if (_movie->_videoPlayback) {
updateWidgets(true);
_window->render();
}
processFrozenScripts();
return;
}
}
// For previous frame
if (!_window->_newMovieStarted && !_vm->_playbackPaused) {
// When Lingo::func_goto* is called, _nextFrame is set
// and _skipFrameAdvance is set to true.
// exitFrame is not called in this case.
if (!_vm->_skipFrameAdvance && _vm->getVersion() >= 400) {
_movie->processEvent(kEventExitFrame);
}
// If there is a transition, the perFrameHook is called
// after each transition subframe instead.
if (_frames[_currentFrame]->_transType == 0) {
_lingo->executePerFrameHook(_currentFrame, 0);
}
}
_vm->_skipFrameAdvance = false;
// the exitFrame event handler may have stopped this movie
if (_playState == kPlayStopped) {
processFrozenScripts();
return;
}
if (!_vm->_playbackPaused) {
if (_nextFrame)
_currentFrame = _nextFrame;
else if (!_window->_newMovieStarted)
_currentFrame++;
}
_nextFrame = 0;
if (_currentFrame >= _frames.size()) {
Window *window = _vm->getCurrentWindow();
if (!window->_movieStack.empty()) {
MovieReference ref = window->_movieStack.back();
window->_movieStack.pop_back();
if (!ref.movie.empty()) {
_playState = kPlayStopped;
window->setNextMovie(ref.movie);
window->_nextMovie.frameI = ref.frameI;
processFrozenScripts();
return;
}
_currentFrame = ref.frameI;
} else {
if (debugChannelSet(-1, kDebugNoLoop)) {
_playState = kPlayStopped;
processFrozenScripts();
return;
}
_currentFrame = 1;
}
}
Common::SortedArray<Label *>::iterator i;
if (_labels != nullptr) {
for (i = _labels->begin(); i != _labels->end(); ++i) {
if ((*i)->number == _currentFrame) {
_currentLabel = _currentFrame;
}
}
}
byte tempo = _frames[_currentFrame]->_scoreCachedTempo;
// puppetTempo is overridden by changes in score tempo
if (_frames[_currentFrame]->_tempo || tempo != _lastTempo) {
_puppetTempo = 0;
} else if (_puppetTempo) {
tempo = _puppetTempo;
}
if (tempo) {
const bool waitForClickOnly = _vm->getVersion() < 300;
const int maxDelay = _vm->getVersion() < 400 ? 120 : 60;
if (tempo >= 256 - maxDelay) {
// Delay
_nextFrameTime = g_system->getMillis() + (256 - tempo) * 1000;
} else if (tempo <= 120) {
// FPS
_currentFrameRate = tempo;
_nextFrameTime = g_system->getMillis() + 1000.0 / (float)_currentFrameRate;
} else {
if (tempo == 128) {
_waitForClick = true;
_waitForClickCursor = false;
renderCursor(_movie->getWindow()->getMousePos());
} else if (!waitForClickOnly && tempo == 135) {
// Wait for sound channel 1
_waitForChannel = 1;
} else if (!waitForClickOnly && tempo == 134) {
// Wait for sound channel 2
_waitForChannel = 2;
} else if (!waitForClickOnly && tempo >= 136 && tempo <= 135 + _numChannelsDisplayed) {
// Wait for a digital video in a channel to finish playing
_waitForVideoChannel = tempo - 135;
} else {
warning("Unhandled tempo instruction: %d", tempo);
}
_nextFrameTime = g_system->getMillis();
}
} else {
_nextFrameTime = g_system->getMillis() + 1000.0 / (float)_currentFrameRate;
}
_lastTempo = tempo;
if (debugChannelSet(-1, kDebugSlow))
_nextFrameTime += 1000;
debugC(1, kDebugLoading, "****************************** Current frame: %d, time: %d", _currentFrame, g_system->getMillis(false));
g_debugger->frameHook();
_lingo->executeImmediateScripts(_frames[_currentFrame]);
if (_vm->getVersion() >= 600) {
// _movie->processEvent(kEventBeginSprite);
// TODO Director 6 step: send beginSprite event to any sprites whose span begin in the upcoming frame
// _movie->processEvent(kEventPrepareFrame);
// TODO: Director 6 step: send prepareFrame event to all sprites and the script channel in upcoming frame
}
// Window is drawn between the prepareFrame and enterFrame events (Lingo in a Nutshell, p.100)
renderFrame(_currentFrame);
_window->_newMovieStarted = false;
// Enter and exit from previous frame
if (!_vm->_playbackPaused) {
uint32 count = _window->frozenLingoStateCount();
// Triggers the frame script in D2-3, explicit enterFrame handlers in D4+
// D4 will only process recursive enterFrame handlers to a depth of 2.
// Any more will be ignored.
if ((_vm->getVersion() >= 400) && (count < 2))
_movie->processEvent(kEventEnterFrame);
// If another frozen state gets triggered, wait another update() before thawing
if (_window->frozenLingoStateCount() > count)
return;
}
// Attempt to thaw and continue any frozen execution after startMovie and enterFrame.
// If they don't complete (i.e. another freezing event like a "go to frame"),
// force another cycle of Score::update().
if (!processFrozenScripts())
return;
if (!_vm->_playbackPaused) {
if ((_vm->getVersion() >= 300 && _vm->getVersion() < 400) || _movie->_allowOutdatedLingo) {
// Movie version of enterFrame, for D3 only. The D3 Interactivity Manual claims
// "This handler executes before anything else when the playback head moves."
// but this is incorrect. The frame script is executed first.
_movie->processEvent(kEventStepMovie);
}
if (_movie->_timeOutPlay)
_movie->_lastTimeOut = _vm->getMacTicks();
}
// TODO Director 6 - another order
// TODO: Figure out when exactly timeout events are processed
if (_vm->getMacTicks() - _movie->_lastTimeOut >= _movie->_timeOutLength) {
_movie->processEvent(kEventTimeout);
_movie->_lastTimeOut = _vm->getMacTicks();
}
}
void Score::renderFrame(uint16 frameId, RenderMode mode) {
// Force cursor update if a new movie's started.
if (_window->_newMovieStarted)
renderCursor(_movie->getWindow()->getMousePos(), true);
if (!renderTransition(frameId)) {
bool skip = renderPrePaletteCycle(frameId, mode);
setLastPalette(frameId);
renderSprites(frameId, mode);
_window->render();
if (!skip)
renderPaletteCycle(frameId, mode);
}
playSoundChannel(frameId, false);
playQueuedSound(); // this is currently only used in FPlayXObj
if (_cursorDirty) {
renderCursor(_movie->getWindow()->getMousePos());
_cursorDirty = false;
}
}
bool Score::renderTransition(uint16 frameId) {
Frame *currentFrame = _frames[frameId];
TransParams *tp = _window->_puppetTransition;
if (tp) {
_window->playTransition(frameId, tp->duration, tp->area, tp->chunkSize, tp->type, 0);
delete _window->_puppetTransition;
_window->_puppetTransition = nullptr;
return true;
} else if (currentFrame->_transType) {
setLastPalette(frameId);
_window->playTransition(frameId, currentFrame->_transDuration, currentFrame->_transArea, currentFrame->_transChunkSize, currentFrame->_transType, resolvePaletteId(currentFrame->_palette.paletteId));
return true;
} else {
return false;
}
}
void Score::renderSprites(uint16 frameId, RenderMode mode) {
if (_window->_newMovieStarted)
mode = kRenderForceUpdate;
_movie->_videoPlayback = false;
for (uint16 i = 0; i < _channels.size(); i++) {
Channel *channel = _channels[i];
Sprite *currentSprite = channel->_sprite;
Sprite *nextSprite = _frames[frameId]->_sprites[i];
// widget content has changed and needs a redraw.
// this doesn't include changes in dimension or position!
bool widgetRedrawn = channel->updateWidget();
if (channel->isActiveVideo()) {
channel->updateVideoTime();
_movie->_videoPlayback = true;
}
if (channel->isDirty(nextSprite) || widgetRedrawn || mode == kRenderForceUpdate) {
if (currentSprite && !currentSprite->_trails)
_window->addDirtyRect(channel->getBbox());
if (currentSprite && currentSprite->_cast && currentSprite->_cast->_erase) {
_movie->eraseCastMember(currentSprite->_castId);
currentSprite->_cast->_erase = false;
currentSprite->setCast(currentSprite->_castId);
nextSprite->setCast(nextSprite->_castId);
}
channel->setClean(nextSprite, i);
// Check again to see if a video has just been started by setClean.
if (channel->isActiveVideo())
_movie->_videoPlayback = true;
_window->addDirtyRect(channel->getBbox());
if (currentSprite) {
debugC(2, kDebugImages,
"Score::renderSprites(): CH: %-3d castId: %s [ink: %d, puppet: %d, moveable: %d, visible: %d] [bbox: %d,%d,%d,%d] [type: %d fg: %d bg: %d] [script: %s]",
i, currentSprite->_castId.asString().c_str(), currentSprite->_ink, currentSprite->_puppet, currentSprite->_moveable, channel->_visible,
PRINT_RECT(channel->getBbox()), currentSprite->_spriteType, currentSprite->_foreColor, currentSprite->_backColor,
currentSprite->_scriptId.asString().c_str());
} else {
debugC(2, kDebugImages, "Score::renderSprites(): CH: %-3d: No sprite", i);
}
} else {
channel->setClean(nextSprite, i, true);
}
// update editable text channel after we render the sprites. because for the current frame, we may get those sprites only when we finished rendering
// (because we are creating widgets and setting active state when we rendering sprites)
if (channel->isActiveText())
_movie->_currentEditableTextChannel = i;
}
}
bool Score::renderPrePaletteCycle(uint16 frameId, RenderMode mode) {
if (_puppetPalette)
return false;
// If the palette is defined in the frame and doesn't match
// the current one, set it
int currentPalette = _frames[frameId]->_palette.paletteId;
if (!currentPalette || !resolvePaletteId(currentPalette))
return false;
if (!_frames[frameId]->_palette.colorCycling &&
!_frames[frameId]->_palette.overTime) {
// Copy the current palette into the snapshot buffer
memset(_paletteSnapshotBuffer, 0, 768);
memcpy(_paletteSnapshotBuffer, g_director->getPalette(), g_director->getPaletteColorCount() * 3);
PaletteV4 *destPal = g_director->getPalette(resolvePaletteId(currentPalette));
int frameRate = CLIP<int>(_frames[frameId]->_palette.speed, 1, 30);
if (debugChannelSet(-1, kDebugFast))
frameRate = 30;
int frameDelay = 1000/60;
int fadeFrames = kFadeColorFrames[frameRate - 1];
byte calcPal[768];
if (_frames[frameId]->_palette.normal) {
// For fade palette transitions, the whole fade happens with
// the previous frame's layout.
for (int i = 0; i < fadeFrames; i++) {
lerpPalette(
calcPal,
_paletteSnapshotBuffer, 256,
destPal->palette, destPal->length,
i + 1,
fadeFrames
);
g_director->setPalette(calcPal, 256);
g_director->draw();
// On click, stop loop and reset palette
if (_vm->processEvents(true)) {
g_director->setPalette(resolvePaletteId(currentPalette));
return true;
}
g_director->delayMillis(frameDelay);
}
} else {
// For fade to black and fade to white palette transitions,
// the first half happens with the previous frame's layout.
byte *fadePal = nullptr;
if (_frames[frameId]->_palette.fadeToBlack) {
// Fade everything except color index 0 to black
fadePal = kBlackPalette;
} else if (_frames[frameId]->_palette.fadeToWhite) {
// Fade everything except color index 255 to white
fadePal = kWhitePalette;
} else {
// Shouldn't reach here
return false;
}
for (int i = 0; i < fadeFrames; i++) {
lerpPalette(
calcPal,
_paletteSnapshotBuffer, 256,
fadePal, 256,
i + 1,
fadeFrames
);
g_director->setPalette(calcPal, 256);
g_director->draw();
// On click, stop loop and reset palette
if (_vm->processEvents(true)) {
g_director->setPalette(resolvePaletteId(currentPalette));
return true;
}
g_director->delayMillis(frameDelay);
}
}
}
return false;
}
void Score::setLastPalette(uint16 frameId) {
if (_puppetPalette)
return;
// If the palette is defined in the frame and doesn't match
// the current one, set it
int currentPalette = _frames[frameId]->_palette.paletteId;
if (!currentPalette || !resolvePaletteId(currentPalette))
return;
bool paletteChanged = currentPalette != _lastPalette && currentPalette;
if (paletteChanged) {
_lastPalette = currentPalette;
_paletteTransitionIndex = 0;
// For color cycling mode, if there's a new palette, switch to it immediately
if (_frames[frameId]->_palette.colorCycling)
g_director->setPalette(resolvePaletteId(_lastPalette));
}
}
bool Score::isPaletteColorCycling() {
return _frames[_currentFrame]->_palette.colorCycling;
}
void Score::renderPaletteCycle(uint16 frameId, RenderMode mode) {
if (_puppetPalette)
return;
// If the palette is defined in the frame and doesn't match
// the current one, set it
int currentPalette = _frames[frameId]->_palette.paletteId;
if (!currentPalette || !resolvePaletteId(currentPalette))
return;
// For palette cycling, the only thing that is checked is if
// the palette ID is the same. Different cycling configs with
// the same palette ID will persist any mutated state.
// e.g. if you use overTime to cycle the palette partially
// through a cycle, then switch to doing a full color cycle
// on the same palette, it will not reset and the weird
// offset will remain.
// Cycle speed in FPS
int speed = _frames[frameId]->_palette.speed;
if (speed == 0)
return;
if (debugChannelSet(-1, kDebugFast))
speed = 30;
// 30 (the maximum) is actually unbounded
int delay = speed == 30 ? 10 : 1000 / speed;
if (_frames[frameId]->_palette.colorCycling) {
// Cycle the colors of a chosen palette
int firstColor = _frames[frameId]->_palette.firstColor;
int lastColor = _frames[frameId]->_palette.lastColor;
if (_frames[frameId]->_palette.overTime) {
// Do a single color step in one frame transition
g_director->shiftPalette(firstColor, lastColor, false);
g_director->draw();
} else {
// Short circuit for few frames renderer
if (debugChannelSet(-1, kDebugFast)) {
g_director->setPalette(resolvePaletteId(currentPalette));
return;
}
// Do a full color cycle in one frame transition
int steps = lastColor - firstColor + 1;
for (int i = 0; i < _frames[frameId]->_palette.cycleCount; i++) {
for (int j = 0; j < steps; j++) {
g_director->shiftPalette(firstColor, lastColor, false);
g_director->draw();
// On click, stop loop and reset palette
if (_vm->processEvents(true)) {
g_director->setPalette(resolvePaletteId(currentPalette));
return;
}
g_director->delayMillis(delay);
}
if (_frames[frameId]->_palette.autoReverse) {
for (int j = 0; j < steps; j++) {
g_director->shiftPalette(firstColor, lastColor, true);
g_director->draw();
// On click, stop loop and reset palette
if (_vm->processEvents(true)) {
g_director->setPalette(resolvePaletteId(currentPalette));
return;
}
g_director->delayMillis(delay);
}
}
}
}
} else {
// Transition from the current palette to a new palette
PaletteV4 *destPal = g_director->getPalette(resolvePaletteId(currentPalette));
int frameCount = _frames[frameId]->_palette.frameCount;
byte calcPal[768];
if (_frames[frameId]->_palette.overTime) {
// Transition over a series of frames
if (_paletteTransitionIndex == 0) {
// Copy the current palette into the snapshot buffer
memset(_paletteSnapshotBuffer, 0, 768);
memcpy(_paletteSnapshotBuffer, g_director->getPalette(), g_director->getPaletteColorCount() * 3);
}
if (_frames[frameId]->_palette.normal) {
// Fade the palette directly to the new palette
lerpPalette(
calcPal,
_paletteSnapshotBuffer, 256,
destPal->palette, destPal->length,
_paletteTransitionIndex + 1,
frameCount
);
} else {
// Fade the palette to an intermediary color (black or white),
// then to the new palette
int halfway = frameCount / 2;
byte *fadePal = nullptr;
if (_frames[frameId]->_palette.fadeToBlack) {
// Fade everything except color index 0 to black
fadePal = kBlackPalette;
} else if (_frames[frameId]->_palette.fadeToWhite) {
// Fade everything except color index 255 to white
fadePal = kWhitePalette;
} else {
// Shouldn't reach here
return;
}
if (_paletteTransitionIndex < halfway) {
lerpPalette(
calcPal,
_paletteSnapshotBuffer, 256,
fadePal, 256,
_paletteTransitionIndex + 1,
halfway
);
} else {
lerpPalette(
calcPal,
fadePal, 256,
destPal->palette, destPal->length,
_paletteTransitionIndex - halfway + 1,
frameCount - halfway
);
}
}
g_director->setPalette(calcPal, 256);
_paletteTransitionIndex++;
_paletteTransitionIndex %= frameCount;
} else {
// Short circuit for fast renderer
if (debugChannelSet(-1, kDebugFast)) {
g_director->setPalette(resolvePaletteId(currentPalette));
return;
}
// Do a full cycle in one frame transition
// For normal mode, we've already faded the palette in renderPrePaletteCycle
if (!_frames[frameId]->_palette.normal) {
byte *fadePal = nullptr;
if (_frames[frameId]->_palette.fadeToBlack) {
// Fade everything except color index 0 to black
fadePal = kBlackPalette;
} else if (_frames[frameId]->_palette.fadeToWhite) {
// Fade everything except color index 255 to white
fadePal = kWhitePalette;
} else {
// Shouldn't reach here
return;
}
int frameRate = CLIP<int>(_frames[frameId]->_palette.speed, 1, 30);
if (debugChannelSet(-1, kDebugFast))
frameRate = 30;
int frameDelay = 1000/60;
int fadeFrames = kFadeColorFrames[frameRate - 1];
// Wait for a fixed time
g_director->setPalette(fadePal, 256);
g_director->draw();
for (int i = 0; i < fadeColorWait; i++) {
// On click, stop loop and reset palette
if (_vm->processEvents(true)) {
g_director->setPalette(resolvePaletteId(currentPalette));
return;
}
g_director->delayMillis(frameDelay);
}
for (int i = 0; i < fadeFrames; i++) {
lerpPalette(
calcPal,
fadePal, 256,
destPal->palette, destPal->length,
i + 1,
fadeFrames
);
g_director->setPalette(calcPal, 256);
g_director->draw();
// On click, stop loop and reset palette
if (_vm->processEvents(true)) {
g_director->setPalette(resolvePaletteId(currentPalette));
return;
}
g_director->delayMillis(frameDelay);
}
}
}
}
}
void Score::renderCursor(Common::Point pos, bool forceUpdate) {
if (_window != _vm->getCursorWindow()) {
// The cursor is outside of this window.
return;
}
if (_waitForClick) {
_vm->setCursor(_waitForClickCursor ? kCursorMouseDown : kCursorMouseUp);
return;
}
if (!_channels.empty() && _playState != kPlayStopped) {
uint spriteId = 0;
for (int i = _channels.size() - 1; i >= 0; i--)
if (_channels[i]->isMouseIn(pos) && !_channels[i]->_cursor.isEmpty()) {
spriteId = i;
break;
}
if (!_channels[spriteId]->_cursor.isEmpty()) {
if (!forceUpdate && _currentCursor == _channels[spriteId]->_cursor)
return;
// try to use the cursor read from exe file.
// currently, we are using mac arrow to represent custom win cursor since we didn't find where it stores. So i comment it out here.
// if (g_director->getPlatform() == Common::kPlatformWindows && _channels[spriteId]->_cursor._cursorType == Graphics::kMacCursorCustom)
// _vm->_wm->replaceCursor(_channels[spriteId]->_cursor._cursorType, g_director->_winCursor[_channels[spriteId]->_cursor._cursorResId]);
_vm->_wm->replaceCursor(_channels[spriteId]->_cursor._cursorType, &_channels[spriteId]->_cursor);
_currentCursor = _channels[spriteId]->_cursor.getRef();
return;
}
}
if (!forceUpdate && _currentCursor == _defaultCursor)
return;
_vm->_wm->replaceCursor(_defaultCursor._cursorType, &_defaultCursor);
_currentCursor = _defaultCursor.getRef();
}
void Score::updateWidgets(bool hasVideoPlayback) {
for (uint16 i = 0; i < _channels.size(); i++) {
Channel *channel = _channels[i];
CastMember *cast = channel->_sprite->_cast;
if (hasVideoPlayback)
channel->updateVideoTime();
if (cast && (cast->_type != kCastDigitalVideo || hasVideoPlayback) && cast->isModified()) {
channel->replaceWidget();
_window->addDirtyRect(channel->getBbox());
}
}
}
void Score::invalidateRectsForMember(CastMember *member) {
for (uint16 i = 0; i < _channels.size(); i++) {
Channel *channel = _channels[i];
if (channel->_sprite->_cast == member) {
_window->addDirtyRect(channel->getBbox());
}
}
}
void Score::screenShot() {
Graphics::Surface rawSurface = _window->getSurface()->rawSurface();
const Graphics::PixelFormat requiredFormat_4byte(4, 8, 8, 8, 8, 0, 8, 16, 24);
Graphics::Surface *newSurface = rawSurface.convertTo(requiredFormat_4byte, _vm->getPalette());
Common::String currentPath = _vm->getCurrentPath().c_str();
Common::replace(currentPath, Common::String(g_director->_dirSeparator), "-"); // exclude dir separator from screenshot filename prefix
Common::String prefix = Common::String::format("%s%s", currentPath.c_str(), _movie->getMacName().c_str());
Common::String filename = dumpScriptName(prefix.c_str(), kMovieScript, g_director->_framesRan, "png");
Common::DumpFile screenshotFile;
if (screenshotFile.open(filename)) {
#ifdef USE_PNG
Image::writePNG(screenshotFile, *newSurface);
#else
warning("Screenshot requested, but PNG support is not compiled in");
#endif
}
newSurface->free();
delete newSurface;
}
uint16 Score::getSpriteIDFromPos(Common::Point pos) {
for (int i = _channels.size() - 1; i >= 0; i--)
if (_channels[i]->isMouseIn(pos))
return i;
return 0;
}
uint16 Score::getMouseSpriteIDFromPos(Common::Point pos) {
for (int i = _channels.size() - 1; i >= 0; i--)
if (_channels[i]->isMouseIn(pos) && _channels[i]->_sprite->respondsToMouse())
return i;
return 0;
}
uint16 Score::getActiveSpriteIDFromPos(Common::Point pos) {
for (int i = _channels.size() - 1; i >= 0; i--)
if (_channels[i]->isMouseIn(pos) && _channels[i]->_sprite->isActive())
return i;
return 0;
}
bool Score::checkSpriteIntersection(uint16 spriteId, Common::Point pos) {
if (_channels[spriteId]->getBbox().contains(pos))
return true;
return false;
}
Common::List<Channel *> Score::getSpriteIntersections(const Common::Rect &r) {
Common::List<Channel *>intersections;
for (uint i = 0; i < _channels.size(); i++) {
if (!_channels[i]->isEmpty() && !r.findIntersectingRect(_channels[i]->getBbox()).isEmpty())
intersections.push_back(_channels[i]);
}
return intersections;
}
uint16 Score::getSpriteIdByMemberId(CastMemberID id) {
for (uint i = 0; i < _channels.size(); i++)
if (_channels[i]->_sprite->_castId == id)
return i;
return 0;
}
Sprite *Score::getSpriteById(uint16 id) {
Channel *channel = getChannelById(id);
if (channel) {
return channel->_sprite;
} else {
warning("Score::getSpriteById(): sprite on frame %d with id %d not found", _currentFrame, id);
return nullptr;
}
}
Sprite *Score::getOriginalSpriteById(uint16 id) {
Frame *frame = _frames[_currentFrame];
if (id < frame->_sprites.size())
return frame->_sprites[id];
warning("Score::getOriginalSpriteById(%d): out of bounds, >= %d", id, frame->_sprites.size());
return nullptr;
}
Channel *Score::getChannelById(uint16 id) {
if (id >= _channels.size()) {
warning("Score::getChannelById(%d): out of bounds, >= %d", id, _channels.size());
return nullptr;
}
return _channels[id];
}
void Score::playSoundChannel(uint16 frameId, bool puppetOnly) {
Frame *frame = _frames[frameId];
debugC(5, kDebugLoading, "playSoundChannel(): Sound1 %s Sound2 %s", frame->_sound1.asString().c_str(), frame->_sound2.asString().c_str());
DirectorSound *sound = _window->getSoundManager();
if (sound->isChannelPuppet(1)) {
sound->playPuppetSound(1);
} else if (!puppetOnly) {
if (frame->_soundType1 >= kMinSampledMenu && frame->_soundType1 <= kMaxSampledMenu) {
sound->playExternalSound(frame->_soundType1, frame->_sound1.member, 1);
} else {
sound->playCastMember(frame->_sound1, 1);
}
}
if (sound->isChannelPuppet(2)) {
sound->playPuppetSound(2);
} else if (!puppetOnly) {
if (frame->_soundType2 >= kMinSampledMenu && frame->_soundType2 <= kMaxSampledMenu) {
sound->playExternalSound(frame->_soundType2, frame->_sound2.member, 2);
} else {
sound->playCastMember(frame->_sound2, 2);
}
}
// Channels above 2 are only usable by Lingo.
if (g_director->getVersion() >= 300) {
sound->playPuppetSound(3);
sound->playPuppetSound(4);
}
}
void Score::playQueuedSound() {
DirectorSound *sound = _window->getSoundManager();
sound->playFPlaySound();
}
void Score::loadFrames(Common::SeekableReadStreamEndian &stream, uint16 version) {
debugC(1, kDebugLoading, "****** Loading frames VWSC");
//stream.hexdump(stream.size());
uint32 size = stream.readUint32();
size -= 4;
if (version < kFileVer400) {
_numChannelsDisplayed = 30;
} else if (version >= kFileVer400 && version < kFileVer500) {
uint32 frame1Offset = stream.readUint32();
uint32 numFrames = stream.readUint32();
uint16 framesVersion = stream.readUint16();
uint16 spriteRecordSize = stream.readUint16();
uint16 numChannels = stream.readUint16();
size -= 14;
if (framesVersion > 13) {
_numChannelsDisplayed = stream.readUint16();
} else {
if (framesVersion <= 7) // Director5
_numChannelsDisplayed = 48;
else
_numChannelsDisplayed = 120; // D6
stream.readUint16(); // Skip
}
size -= 2;
warning("STUB: Score::loadFrames. frame1Offset: %x numFrames: %x version: %x spriteRecordSize: %x numChannels: %x numChannelsDisplayed: %x",
frame1Offset, numFrames, framesVersion, spriteRecordSize, numChannels, _numChannelsDisplayed);
// Unknown, some bytes - constant (refer to contuinity).
} else if (version >= kFileVer500) {
//what data is up the top of D5 VWSC?
uint32 unk1 = stream.readUint32();
uint32 unk2 = stream.readUint32();
uint16 unk3, unk4, unk5, unk6;
if (unk2 > 0) {
uint32 blockSize = stream.readUint32() - 1;
stream.readUint32();
stream.readUint32();
stream.readUint32();
stream.readUint32();
for (uint32 skip = 0; skip < blockSize * 4; skip++)
stream.readByte();
//header number two... this is our actual score entry point.
unk1 = stream.readUint32();
unk2 = stream.readUint32();
stream.readUint32();
unk3 = stream.readUint16();
unk4 = stream.readUint16();
unk5 = stream.readUint16();
unk6 = stream.readUint16();
} else {
unk3 = stream.readUint16();
unk4 = stream.readUint16();
unk5 = stream.readUint16();
unk6 = stream.readUint16();
size -= 16;
}
warning("STUB: Score::loadFrames. unk1: %x unk2: %x unk3: %x unk4: %x unk5: %x unk6: %x", unk1, unk2, unk3, unk4, unk5, unk6);
}
uint16 channelSize;
uint16 channelOffset;
Frame *initial = new Frame(this, _numChannelsDisplayed);
// Push a frame at frame#0 position.
// This makes all indexing simpler
_frames.push_back(initial);
// This is a representation of the channelData. It gets overridden
// partically by channels, hence we keep it and read the score from left to right
//
// TODO Merge it with shared cast
byte channelData[kChannelDataSize];
memset(channelData, 0, kChannelDataSize);
uint8 currentTempo = 0;
while (size != 0 && !stream.eos()) {
uint16 frameSize = stream.readUint16();
debugC(3, kDebugLoading, "++++++++++ score frame %d (frameSize %d) size %d", _frames.size(), frameSize, size);
if (frameSize > 0) {
Frame *frame = new Frame(this, _numChannelsDisplayed);
size -= frameSize;
frameSize -= 2;
while (frameSize != 0) {
if (_vm->getVersion() < 400) {
channelSize = stream.readByte() * 2;
channelOffset = stream.readByte() * 2;
frameSize -= channelSize + 2;
} else {
channelSize = stream.readUint16();
channelOffset = stream.readUint16();
frameSize -= channelSize + 4;
}
assert(channelOffset + channelSize < kChannelDataSize);
stream.read(&channelData[channelOffset], channelSize);
}
Common::MemoryReadStreamEndian *str = new Common::MemoryReadStreamEndian(channelData, ARRAYSIZE(channelData), stream.isBE());
// str->hexdump(str->size(), 32);
frame->readChannels(str, version);
delete str;
// Precache the current FPS tempo, as this carries forward to frames to the right
// of the instruction.
// Delay type tempos (e.g. wait commands, delays) apply to only a single frame, and are ignored here.
if (frame->_tempo && frame->_tempo <= 120)
currentTempo = frame->_tempo;
frame->_scoreCachedTempo = frame->_tempo ? frame->_tempo : currentTempo;
debugC(8, kDebugLoading, "Score::loadFrames(): Frame %d actionId: %s", _frames.size(), frame->_actionId.asString().c_str());
_frames.push_back(frame);
} else {
warning("zero sized frame!? exiting loop until we know what to do with the tags that follow.");
size = 0;
}
}
}
void Score::setSpriteCasts() {
// Update sprite cache of cast pointers/info
for (uint16 i = 0; i < _frames.size(); i++) {
for (uint16 j = 0; j < _frames[i]->_sprites.size(); j++) {
_frames[i]->_sprites[j]->setCast(_frames[i]->_sprites[j]->_castId);
debugC(1, kDebugImages, "Score::setSpriteCasts(): Frame: %d Channel: %d castId: %s type: %d", i, j, _frames[i]->_sprites[j]->_castId.asString().c_str(), _frames[i]->_sprites[j]->_spriteType);
}
}
}
void Score::loadLabels(Common::SeekableReadStreamEndian &stream) {
if (debugChannelSet(5, kDebugLoading)) {
debug("Score::loadLabels()");
stream.hexdump(stream.size());
}
_labels = new Common::SortedArray<Label *>(compareLabels);
uint16 count = stream.readUint16() + 1;
uint32 offset = count * 4 + 2;
uint16 frame = stream.readUint16();
uint32 stringPos = stream.readUint16() + offset;
for (uint16 i = 1; i < count; i++) {
uint16 nextFrame = stream.readUint16();
uint32 nextStringPos = stream.readUint16() + offset;
uint32 streamPos = stream.pos();
stream.seek(stringPos);
Common::String label;
Common::String comment = "";
char ch;
uint32 j = stringPos;
// handle label
while(j < nextStringPos) {
j++;
ch = stream.readByte();
if (ch == '\r')
break;
label += ch;
}
// handle label comments
while(j < nextStringPos) {
j++;
ch = stream.readByte();
if (ch == '\r')
ch = '\n';
comment += ch;
}
label = _movie->getCast()->decodeString(label).encode(Common::kUtf8);
_labels->insert(new Label(label, frame, comment));
stream.seek(streamPos);
frame = nextFrame;
stringPos = nextStringPos;
}
Common::SortedArray<Label *>::iterator j;
debugC(2, kDebugLoading, "****** Loading labels");
for (j = _labels->begin(); j != _labels->end(); ++j) {
debugC(2, kDebugLoading, "Frame %d, Label '%s', Comment '%s'", (*j)->number, utf8ToPrintable((*j)->name).c_str(), (*j)->comment.c_str());
}
}
int Score::compareLabels(const void *a, const void *b) {
return ((const Label *)a)->number - ((const Label *)b)->number;
}
void Score::loadActions(Common::SeekableReadStreamEndian &stream) {
debugC(2, kDebugLoading, "****** Loading Actions VWAC");
uint16 count = stream.readUint16() + 1;
uint32 offset = count * 4 + 2;
byte id = stream.readByte();
byte subId = stream.readByte(); // I couldn't find how it used in continuity (except print). Frame actionId = 1 byte.
uint32 stringPos = stream.readUint16() + offset;
for (uint16 i = 1; i <= count; i++) {
uint16 nextId = stream.readByte();
byte nextSubId = stream.readByte();
uint32 nextStringPos = stream.readUint16() + offset;
uint32 streamPos = stream.pos();
stream.seek(stringPos);
Common::String script = stream.readString(0, nextStringPos - stringPos);
_actions[i] = _movie->getCast()->decodeString(script).encode(Common::kUtf8);
debugC(3, kDebugLoading, "Action index: %d id: %d nextId: %d subId: %d, code: %s", i, id, nextId, subId, _actions[i].c_str());
stream.seek(streamPos);
id = nextId;
subId = nextSubId;
stringPos = nextStringPos;
if ((int32)stringPos == stream.size())
break;
}
bool *scriptRefs = (bool *)calloc(_actions.size() + 1, sizeof(bool));
// Now let's scan which scripts are actually referenced
for (uint i = 0; i < _frames.size(); i++) {
if ((uint)_frames[i]->_actionId.member <= _actions.size())
scriptRefs[_frames[i]->_actionId.member] = true;
for (uint16 j = 0; j <= _frames[i]->_numChannels; j++) {
if ((uint)_frames[i]->_sprites[j]->_scriptId.member <= _actions.size())
scriptRefs[_frames[i]->_sprites[j]->_scriptId.member] = true;
}
}
Common::HashMap<uint16, Common::String>::iterator j;
if (ConfMan.getBool("dump_scripts"))
for (j = _actions.begin(); j != _actions.end(); ++j) {
if (!j->_value.empty())
_movie->getCast()->dumpScript(j->_value.c_str(), kScoreScript, j->_key);
}
for (j = _actions.begin(); j != _actions.end(); ++j) {
if (!scriptRefs[j->_key]) {
// Check if it is empty
bool empty = true;
Common::U32String u32Script(j->_value);
for (const Common::u32char_type_t *ptr = u32Script.c_str(); *ptr; ptr++)
if (!(*ptr == ' ' || *ptr == '-' || *ptr == '\n' || *ptr == '\r' || *ptr == '\t' || *ptr == CONTINUATION)) {
empty = false;
break;
}
if (!empty)
warning("Action id %d is not referenced, the code is:\n-----\n%s\n------", j->_key, j->_value.c_str());
continue;
}
if (!j->_value.empty()) {
_movie->getMainLingoArch()->addCode(j->_value, kScoreScript, j->_key);
processImmediateFrameScript(j->_value, j->_key);
}
}
free(scriptRefs);
}
Common::String Score::formatChannelInfo() {
Frame &frame = *_frames[_currentFrame];
Common::String result;
result += Common::String::format("TMPO: tempo: %d, skipFrameFlag: %d, blend: %d\n",
frame._tempo, frame._skipFrameFlag, frame._blend);
if (frame._palette.paletteId) {
result += Common::String::format("PAL: paletteId: %d, firstColor: %d, lastColor: %d, flags: %d, cycleCount: %d, speed: %d, frameCount: %d, fade: %d, delay: %d, style: %d\n",
frame._palette.paletteId, frame._palette.firstColor, frame._palette.lastColor, frame._palette.flags,
frame._palette.cycleCount, frame._palette.speed, frame._palette.frameCount,
frame._palette.fade, frame._palette.delay, frame._palette.style);
} else {
result += Common::String::format("PAL: paletteId: 000\n");
}
result += Common::String::format("TRAN: transType: %d, transDuration: %d, transChunkSize: %d\n",
frame._transType, frame._transDuration, frame._transChunkSize);
result += Common::String::format("SND: 1 sound1: %d, soundType1: %d\n", frame._sound1.member, frame._soundType1);
result += Common::String::format("SND: 2 sound2: %d, soundType2: %d\n", frame._sound2.member, frame._soundType2);
result += Common::String::format("LSCR: actionId: %d\n", frame._actionId.member);
for (int i = 0; i < frame._numChannels; i++) {
Channel &channel = *_channels[i + 1];
Sprite &sprite = *channel._sprite;
if (sprite._castId.member) {
result += Common::String::format("CH: %-3d castId: %s, visible: %d, [inkData: 0x%02x [ink: %d, trails: %d, line: %d], %dx%d@%d,%d type: %d fg: %d bg: %d], script: %s, flags2: 0x%x, unk2: 0x%x, unk3: 0x%x, constraint: %d, puppet: %d, stretch: %d\n",
i + 1, sprite._castId.asString().c_str(), channel._visible, sprite._inkData,
sprite._ink, sprite._trails, sprite._thickness, channel._width, channel._height,
channel._currentPoint.x, channel._currentPoint.y,
sprite._spriteType, sprite._foreColor, sprite._backColor, sprite._scriptId.asString().c_str(), sprite._colorcode, sprite._blendAmount, sprite._unk3, channel._constraint, sprite._puppet, sprite._stretch);
} else {
result += Common::String::format("CH: %-3d castId: 000\n", i + 1);
}
}
return result;
}
} // End of namespace Director