mirror of
https://github.com/scummvm/scummvm.git
synced 2025-04-02 10:52:32 -04:00
1962 lines
64 KiB
C++
1962 lines
64 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/>.
|
|
*
|
|
*/
|
|
|
|
#define FORBIDDEN_SYMBOL_EXCEPTION_getenv
|
|
|
|
#include "common/config-manager.h"
|
|
#include "common/file.h"
|
|
#include "common/rational.h"
|
|
#include "common/memstream.h"
|
|
#include "common/punycode.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/debugger.h"
|
|
#include "director/cast.h"
|
|
#include "director/frame.h"
|
|
#include "director/score.h"
|
|
#include "director/movie.h"
|
|
#include "director/sound.h"
|
|
#include "director/channel.h"
|
|
#include "director/sprite.h"
|
|
#include "director/window.h"
|
|
#include "director/castmember/castmember.h"
|
|
#include "director/castmember/filmloop.h"
|
|
#include "director/castmember/transition.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 = 0;
|
|
_puppetPalette = false;
|
|
_paletteTransitionIndex = 0;
|
|
memset(_paletteSnapshotBuffer, 0, 768);
|
|
|
|
_labels = nullptr;
|
|
|
|
_currentFrameRate = 20;
|
|
_nextFrame = 0;
|
|
_currentLabel = 0;
|
|
_nextFrameTime = 0;
|
|
_nextFrameDelay = 0;
|
|
_lastTempo = 0;
|
|
_waitForChannel = 0;
|
|
_waitForVideoChannel = 0;
|
|
_cursorDirty = false;
|
|
_waitForClick = false;
|
|
_waitForClickCursor = false;
|
|
_activeFade = false;
|
|
_exitFrameCalled = false;
|
|
_stopPlayCalled = false;
|
|
_playState = kPlayNotStarted;
|
|
|
|
_numChannelsDisplayed = 0;
|
|
_skipTransition = false;
|
|
|
|
_curFrameNumber = 1;
|
|
_framesStream = nullptr;
|
|
_currentFrame = nullptr;
|
|
}
|
|
|
|
Score::~Score() {
|
|
for (uint i = 0; i < _channels.size(); i++)
|
|
delete _channels[i];
|
|
|
|
if (_labels)
|
|
for (auto &it : *_labels)
|
|
delete it;
|
|
|
|
delete _labels;
|
|
|
|
for (auto &it : _scoreCache)
|
|
delete it;
|
|
|
|
if (_framesStream)
|
|
delete _framesStream;
|
|
|
|
if (_currentFrame) {
|
|
delete _currentFrame;
|
|
}
|
|
}
|
|
|
|
void Score::setPuppetTempo(int16 puppetTempo) {
|
|
_puppetTempo = puppetTempo;
|
|
}
|
|
|
|
CastMemberID Score::getCurrentPalette() {
|
|
return g_director->_lastPalette;
|
|
}
|
|
|
|
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::processFrozenPlayScript() {
|
|
// Unfreeze the play script if the special flag is set
|
|
if (g_lingo->_playDone) {
|
|
g_lingo->_playDone = false;
|
|
if (_window->thawLingoPlayState()) {
|
|
Symbol currentScript = _window->getLingoState()->callstack.front()->sp;
|
|
g_lingo->switchStateFromWindow();
|
|
bool completed = g_lingo->execute();
|
|
if (!completed) {
|
|
debugC(3, kDebugLingoExec, "Score::processFrozenPlayScript(): State froze again mid-thaw, interrupting");
|
|
return false;
|
|
} else if (currentScript == g_lingo->_currentInputEvent) {
|
|
// script that just completed was the current input event, clear the flag
|
|
debugC(3, kDebugEvents, "Score::processFrozenPlayScript(): Input event completed");
|
|
g_lingo->_currentInputEvent = Symbol();
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
bool Score::processFrozenScripts(bool recursion, int count) {
|
|
if (!processFrozenPlayScript())
|
|
return false;
|
|
|
|
// Unfreeze any in-progress scripts and attempt to run them
|
|
// to completion.
|
|
bool limit = count != 0;
|
|
uint32 remainCount = recursion ? _window->frozenLingoRecursionCount() : _window->frozenLingoStateCount();
|
|
while (remainCount && (limit ? count > 0 : true)) {
|
|
_window->thawLingoState();
|
|
LingoState *state = _window->getLingoState();
|
|
Symbol currentScript = state->callstack.front()->sp;
|
|
g_lingo->switchStateFromWindow();
|
|
bool completed = g_lingo->execute();
|
|
if (!completed || (recursion ? _window->frozenLingoRecursionCount() : _window->frozenLingoStateCount()) >= remainCount) {
|
|
debugC(3, kDebugLingoExec, "Score::processFrozenScripts(): State froze again mid-thaw, interrupting");
|
|
// Workaround for if a state gets moved to to the play state
|
|
if (currentScript == g_lingo->_currentInputEvent && state == _window->getLingoPlayState()) {
|
|
debugC(3, kDebugEvents, "Score::processFrozenScripts(): Input event got moved to the play state, clearing block");
|
|
g_lingo->_currentInputEvent = Symbol();
|
|
}
|
|
|
|
return false;
|
|
} else if (currentScript == g_lingo->_currentInputEvent) {
|
|
// script that just completed was the current input event, clear the flag
|
|
debugC(3, kDebugEvents, "Score::processFrozenScripts(): Input event completed");
|
|
g_lingo->_currentInputEvent = Symbol();
|
|
}
|
|
remainCount = recursion ? _window->frozenLingoRecursionCount() : _window->frozenLingoStateCount();
|
|
count -= 1;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
uint16 Score::getLabel(Common::String &label) {
|
|
if (!_labels) {
|
|
warning("Score::getLabel: No labels set");
|
|
return 0;
|
|
}
|
|
|
|
for (auto &i : *_labels) {
|
|
if (i->name.equalsIgnoreCase(label)) {
|
|
return i->number;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
Common::String *Score::getLabelList() {
|
|
Common::String *res = new Common::String;
|
|
|
|
for (auto &i : *_labels) {
|
|
*res += i->name;
|
|
*res += '\r';
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
Common::String *Score::getFrameLabel(uint id) {
|
|
for (auto &i : *_labels) {
|
|
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() {
|
|
if (!_labels)
|
|
return 0;
|
|
|
|
int frame = 0;
|
|
|
|
for (auto &i : *_labels) {
|
|
if (i->number <= _curFrameNumber)
|
|
frame = i->number;
|
|
}
|
|
|
|
return frame;
|
|
}
|
|
|
|
void Score::gotoNext() {
|
|
// we can just try to use the current frame and get the next label
|
|
_nextFrame = getNextLabelNumber(_curFrameNumber);
|
|
}
|
|
|
|
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;
|
|
|
|
for (auto &it : *_labels) {
|
|
if (it->number > referenceFrame) {
|
|
return it->number;
|
|
}
|
|
}
|
|
// if no markers are to the right of the playback head,
|
|
// return the last marker
|
|
return _labels->back()->number;
|
|
}
|
|
|
|
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() {
|
|
_playState = kPlayStarted;
|
|
_nextFrameTime = 0;
|
|
_nextFrameDelay = 0;
|
|
|
|
if (!_currentFrame) {
|
|
warning("Score::startLoop(): Movie has no frames");
|
|
_playState = kPlayStopped;
|
|
|
|
return;
|
|
}
|
|
|
|
// load first frame (either 1 or _nextFrame)
|
|
updateCurrentFrame();
|
|
|
|
// All frames in the same movie have the same number of channels
|
|
if (_playState != kPlayStopped)
|
|
for (uint i = 0; i < _currentFrame->_sprites.size(); i++)
|
|
_channels.push_back(new Channel(this, _currentFrame->_sprites[i], i));
|
|
|
|
updateSprites(kRenderForceUpdate, true);
|
|
|
|
if (_vm->getVersion() >= 300)
|
|
_movie->processEvent(kEventStartMovie);
|
|
}
|
|
|
|
void Score::step() {
|
|
if (_playState == kPlayPaused)
|
|
return;
|
|
|
|
if (_playState == kPlayStopped)
|
|
return;
|
|
|
|
if (!_movie->_inputEventQueue.empty() && !_window->frozenLingoStateCount()) {
|
|
_lingo->processEvents(_movie->_inputEventQueue, true);
|
|
}
|
|
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 (_stopPlayCalled)
|
|
return;
|
|
_stopPlayCalled = true;
|
|
if (_vm->getVersion() >= 300)
|
|
_movie->processEvent(kEventStopMovie);
|
|
_lingo->executePerFrameHook(-1, 0);
|
|
}
|
|
|
|
void Score::setDelay(uint32 ticks) {
|
|
// the score will continually loop at the exitFrame handler,
|
|
// even if the handler sets a new delay value only the first one
|
|
// will be acknowledged.
|
|
if (!_nextFrameDelay) {
|
|
_nextFrameDelay = g_system->getMillis() + (ticks * 1000 / 60);
|
|
debugC(5, kDebugEvents, "Score::setDelay(): delaying %d ticks, next frame time at %d", ticks, _nextFrameDelay);
|
|
}
|
|
}
|
|
|
|
void Score::setCurrentFrame(uint16 frameId) {
|
|
_nextFrame = frameId;
|
|
}
|
|
|
|
bool Score::isWaitingForNextFrame() {
|
|
bool keepWaiting = false;
|
|
debugC(8, kDebugEvents, "Score::isWaitingForNextFrame(): nextFrameTime: %d, time: %d, sound: %d, click: %d, video: %d", _nextFrameTime, g_system->getMillis(false), _waitForChannel, _waitForClick, _waitForVideoChannel);
|
|
|
|
bool goingTo = _nextFrame && _nextFrame != _curFrameNumber;
|
|
|
|
if (_waitForChannel) {
|
|
if (_soundManager->isChannelActive(_waitForChannel) && !goingTo) {
|
|
keepWaiting = true;
|
|
} else {
|
|
_waitForChannel = 0;
|
|
}
|
|
} else if (_waitForClick) {
|
|
if (!goingTo) {
|
|
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 && !goingTo) {
|
|
keepWaiting = true;
|
|
} else {
|
|
_waitForVideoChannel = 0;
|
|
}
|
|
} else if (g_system->getMillis() < _nextFrameTime) {
|
|
keepWaiting = true;
|
|
}
|
|
|
|
if (!keepWaiting) {
|
|
debugC(8, kDebugEvents, "Score::isWaitingForNextFrame(): end of wait cycle");
|
|
}
|
|
return keepWaiting;
|
|
}
|
|
|
|
void Score::updateCurrentFrame() {
|
|
uint32 nextFrameNumberToLoad = _curFrameNumber;
|
|
|
|
if (!_vm->_playbackPaused) {
|
|
if (_nextFrame) {
|
|
// With the advent of demand loading frames and due to partial updates, we rebuild our channel data
|
|
// when jumping.
|
|
nextFrameNumberToLoad = _nextFrame;
|
|
}
|
|
else if (!_window->_newMovieStarted)
|
|
nextFrameNumberToLoad = (_curFrameNumber+1);
|
|
}
|
|
|
|
_nextFrame = 0;
|
|
|
|
if (nextFrameNumberToLoad >= getFramesNum()) {
|
|
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;
|
|
return;
|
|
}
|
|
nextFrameNumberToLoad = ref.frameI;
|
|
} else {
|
|
if (debugChannelSet(-1, kDebugNoLoop)) {
|
|
_playState = kPlayStopped;
|
|
processFrozenScripts();
|
|
return;
|
|
}
|
|
|
|
nextFrameNumberToLoad = 1;
|
|
}
|
|
}
|
|
|
|
if (_labels != nullptr) {
|
|
for (auto &i : *_labels) {
|
|
if (i->number == nextFrameNumberToLoad) {
|
|
_currentLabel = nextFrameNumberToLoad;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (_curFrameNumber != nextFrameNumberToLoad) {
|
|
// Load the current sprite information into the _currentFrame data store.
|
|
// This is specifically because of delta updates; loading the next frame
|
|
// in the score applies delta changes to _currentFrame, and ideally we want
|
|
// those deltas to be applied over the top of whatever the current state is.
|
|
for (uint ch = 0; ch < _channels.size(); ch++)
|
|
*_currentFrame->_sprites[ch] = *_channels[ch]->_sprite;
|
|
|
|
// this copies in the frame data and updates _curFrameNumber
|
|
loadFrame(nextFrameNumberToLoad, true);
|
|
|
|
// finally, update the channels and buffer any dirty rectangles
|
|
updateSprites(kRenderModeNormal, true);
|
|
}
|
|
return;
|
|
}
|
|
|
|
void Score::updateNextFrameTime() {
|
|
byte tempo = _currentFrame->_mainChannels.tempo ? _currentFrame->_mainChannels.tempo : _currentFrame->_mainChannels.scoreCachedTempo;
|
|
// puppetTempo is overridden by changes in score tempo
|
|
if (_currentFrame->_mainChannels.tempo || tempo != _lastTempo) {
|
|
_puppetTempo = 0;
|
|
} else if (_puppetTempo) {
|
|
tempo = _puppetTempo;
|
|
}
|
|
|
|
if (tempo) {
|
|
const bool waitForClickOnly = _vm->getVersion() < 300;
|
|
int maxDelay = 60;
|
|
if (_vm->getVersion() < 300) {
|
|
maxDelay = 120;
|
|
} else if (_vm->getVersion() < 400) {
|
|
// Director 3 has a slider that goes up to 120, but any value
|
|
// beyond 95 gets converted into a video wait instruction.
|
|
maxDelay = 95;
|
|
}
|
|
if (tempo >= 256 - maxDelay) {
|
|
// Delay
|
|
_nextFrameTime = g_system->getMillis() + (256 - tempo) * 1000;
|
|
debugC(5, kDebugEvents, "Score::updateNextFrameTime(): setting _nextFrameTime to %d based on a delay of %d", _nextFrameTime, 256 - tempo);
|
|
} else if (tempo <= 120) {
|
|
// FPS
|
|
_currentFrameRate = tempo;
|
|
if (g_director->_fpsLimit)
|
|
_currentFrameRate = MIN(g_director->_fpsLimit, _currentFrameRate);
|
|
_nextFrameTime = g_system->getMillis() + 1000.0 / (float)_currentFrameRate;
|
|
debugC(5, kDebugEvents, "Score::updateNextFrameTime(): setting _nextFrameTime to %d based on a framerate of %d", _nextFrameTime, _currentFrameRate);
|
|
} else {
|
|
if (tempo == 128) {
|
|
_waitForClick = true;
|
|
_waitForClickCursor = false;
|
|
renderCursor(_movie->getWindow()->getMousePos());
|
|
debugC(5, kDebugEvents, "Score::updateNextFrameTime(): waiting for mouse click before next frame");
|
|
} else if (!waitForClickOnly && tempo == 135) {
|
|
// Wait for sound channel 1
|
|
_waitForChannel = 1;
|
|
debugC(5, kDebugEvents, "Score::updateNextFrameTime(): waiting for sound channel 1 before next frame");
|
|
} else if (!waitForClickOnly && tempo == 134) {
|
|
// Wait for sound channel 2
|
|
_waitForChannel = 2;
|
|
debugC(5, kDebugEvents, "Score::updateNextFrameTime(): waiting for sound channel 2 before next frame");
|
|
} else if (!waitForClickOnly && tempo >= 136 && tempo <= 135 + _numChannelsDisplayed) {
|
|
// Wait for a digital video in a channel to finish playing
|
|
_waitForVideoChannel = tempo - 135;
|
|
debugC(5, kDebugEvents, "Score::updateNextFrameTime(): waiting for video in channel %d before next frame", _waitForVideoChannel);
|
|
} else {
|
|
warning("Score::updateNextFrameTime(): 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;
|
|
}
|
|
|
|
void Score::update() {
|
|
if (_activeFade) {
|
|
_activeFade = _soundManager->fadeChannels();
|
|
}
|
|
|
|
if (!debugChannelSet(-1, kDebugFast)) {
|
|
// end update cycle if we're still waiting for the next frame
|
|
if (isWaitingForNextFrame()) {
|
|
if (_movie->_videoPlayback) {
|
|
updateWidgets(true);
|
|
_window->render();
|
|
}
|
|
|
|
// Don't process frozen script if we use jump instructions
|
|
// like "go to frame", or open a new movie.
|
|
if (!_nextFrame) {
|
|
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 && !_exitFrameCalled) {
|
|
// Exit the current frame. This can include scopeless ScoreScripts.
|
|
_movie->processEvent(kEventExitFrame);
|
|
_exitFrameCalled = true;
|
|
}
|
|
}
|
|
|
|
_vm->_skipFrameAdvance = false;
|
|
|
|
// Check for delay
|
|
if (g_system->getMillis() < _nextFrameDelay) {
|
|
if (_movie->_videoPlayback) {
|
|
updateWidgets(true);
|
|
_window->render();
|
|
}
|
|
|
|
// Don't process frozen script if we use jump instructions
|
|
// like "go to frame", or open a new movie.
|
|
if (!_nextFrame || _nextFrame == _curFrameNumber) {
|
|
processFrozenScripts();
|
|
}
|
|
|
|
return;
|
|
}
|
|
_nextFrameDelay = 0;
|
|
|
|
// the exitFrame event handler may have stopped this movie
|
|
if (_playState == kPlayStopped) {
|
|
return;
|
|
}
|
|
|
|
// change current frame and load frame data, if required
|
|
updateCurrentFrame();
|
|
|
|
// set the delay time/condition until the next frame
|
|
updateNextFrameTime();
|
|
|
|
debugC(1, kDebugEvents, "****************************** Current frame: %d, time: %d", _curFrameNumber, g_system->getMillis(false));
|
|
g_debugger->frameHook();
|
|
|
|
// movie could have been stopped by a window switch or a debug flag
|
|
if (_playState == kPlayStopped) {
|
|
processFrozenScripts();
|
|
return;
|
|
}
|
|
|
|
uint32 count = _window->frozenLingoStateCount();
|
|
|
|
// new frame, first call the perFrameHook (if one exists)
|
|
if (!_window->_newMovieStarted && !_vm->_playbackPaused) {
|
|
// Call the perFrameHook as soon as a frame switch is done.
|
|
// If there is a transition, the perFrameHook is called
|
|
// after each transition subframe instead of here.
|
|
if (_currentFrame->_mainChannels.transType == 0 && _currentFrame->_mainChannels.trans.isNull()) {
|
|
_lingo->executePerFrameHook(_curFrameNumber, 0);
|
|
}
|
|
}
|
|
if (_window->frozenLingoStateCount() > count)
|
|
return;
|
|
|
|
// Check to see if we've hit the recursion limit
|
|
if (_vm->getVersion() >= 400 && _window->frozenLingoRecursionCount() >= 2) {
|
|
debugC(1, kDebugEvents, "Score::update(): hitting D4 recursion depth limit, defrosting");
|
|
processFrozenScripts(true);
|
|
return;
|
|
} else if (_window->frozenLingoStateCount() >= 64) {
|
|
warning("Score::update(): Stopping runaway script recursion. By this point D3 will have run out of stack space");
|
|
processFrozenScripts();
|
|
return;
|
|
}
|
|
|
|
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(_curFrameNumber);
|
|
_window->_newMovieStarted = false;
|
|
|
|
// then call the stepMovie hook (if one exists)
|
|
// D4 and above only call it if _allowOutdatedLingo is enabled.
|
|
count = _window->frozenLingoStateCount();
|
|
if (!_vm->_playbackPaused && (_vm->getVersion() < 400 || _movie->_allowOutdatedLingo)) {
|
|
_movie->processEvent(kEventStepMovie);
|
|
}
|
|
// If this stepMovie call is frozen, drop the next enterFrame event
|
|
if (_window->frozenLingoStateCount() > count)
|
|
return;
|
|
|
|
// If we've hit the recursion limit, don't enterFrame
|
|
if (_vm->getVersion() >= 400 && _window->frozenLingoRecursionCount() >= 2) {
|
|
debugC(1, kDebugEvents, "Score::update: exiting early due to recursion depth limit");
|
|
return;
|
|
}
|
|
|
|
// then call the enterFrame hook (if one exists)
|
|
count = _window->frozenLingoStateCount();
|
|
if (!_vm->_playbackPaused) {
|
|
_exitFrameCalled = false;
|
|
if (_vm->getVersion() >= 400) {
|
|
_movie->processEvent(kEventEnterFrame);
|
|
}
|
|
}
|
|
if (_window->frozenLingoStateCount() > count)
|
|
return;
|
|
|
|
// then execute any immediate scripts, i.e. handlers attached to sprites
|
|
count = _window->frozenLingoStateCount();
|
|
_lingo->executeImmediateScripts(_currentFrame);
|
|
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 (!_nextFrame && !processFrozenScripts())
|
|
return;
|
|
|
|
if (!_vm->_playbackPaused) {
|
|
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) {
|
|
uint32 start = g_system->getMillis(false);
|
|
// Force cursor update if a new movie's started.
|
|
if (_window->_newMovieStarted)
|
|
renderCursor(_movie->getWindow()->getMousePos(), true);
|
|
|
|
|
|
if (_skipTransition) {
|
|
incrementFilmLoops();
|
|
_window->render();
|
|
_skipTransition = false;
|
|
} else if (g_director->_playbackPaused) {
|
|
updateSprites(mode);
|
|
incrementFilmLoops();
|
|
_window->render();
|
|
} else if (!renderTransition(frameId, mode)) {
|
|
bool skip = renderPrePaletteCycle(mode);
|
|
setLastPalette();
|
|
updateSprites(mode);
|
|
incrementFilmLoops();
|
|
_window->render();
|
|
if (!skip)
|
|
renderPaletteCycle(mode);
|
|
}
|
|
|
|
playSoundChannel(false);
|
|
playQueuedSound(); // this is currently only used in FPlayXObj
|
|
|
|
if (_cursorDirty) {
|
|
renderCursor(_movie->getWindow()->getMousePos());
|
|
_cursorDirty = false;
|
|
}
|
|
uint32 end = g_system->getMillis(false);
|
|
debugC(5, kDebugEvents, "Score::renderFrame() finished in %d millis", end - start);
|
|
}
|
|
|
|
bool Score::renderTransition(uint16 frameId, RenderMode mode) {
|
|
Frame *currentFrame = _currentFrame;
|
|
TransParams *tp = _window->_puppetTransition;
|
|
|
|
if (tp) {
|
|
setLastPalette();
|
|
_window->playTransition(frameId, mode, tp->duration, tp->area, tp->chunkSize, tp->type, currentFrame->_mainChannels.scoreCachedPaletteId);
|
|
delete _window->_puppetTransition;
|
|
_window->_puppetTransition = nullptr;
|
|
return true;
|
|
} else if (currentFrame->_mainChannels.transType) {
|
|
setLastPalette();
|
|
_window->playTransition(frameId, mode, currentFrame->_mainChannels.transDuration, currentFrame->_mainChannels.transArea, currentFrame->_mainChannels.transChunkSize, currentFrame->_mainChannels.transType, currentFrame->_mainChannels.scoreCachedPaletteId);
|
|
return true;
|
|
} else if (!currentFrame->_mainChannels.trans.isNull()) {
|
|
CastMember *member = _movie->getCastMember(currentFrame->_mainChannels.trans);
|
|
if (member && member->_type == kCastTransition) {
|
|
TransitionCastMember *trans = static_cast<TransitionCastMember *>(member);
|
|
setLastPalette();
|
|
_window->playTransition(frameId, mode, trans->_durationMillis, trans->_area, trans->_chunkSize, trans->_transType, currentFrame->_mainChannels.scoreCachedPaletteId);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void Score::incrementFilmLoops() {
|
|
for (auto &it : _channels) {
|
|
if (it->_sprite->_cast && (it->_sprite->_cast->_type == kCastFilmLoop || it->_sprite->_cast->_type == kCastMovie)) {
|
|
FilmLoopCastMember *fl = ((FilmLoopCastMember *)it->_sprite->_cast);
|
|
if (!fl->_frames.empty()) {
|
|
// increment the film loop counter
|
|
it->_filmLoopFrame += 1;
|
|
it->_filmLoopFrame %= fl->_frames.size();
|
|
} else {
|
|
warning("Score::updateFilmLoops(): invalid film loop in castId %s", it->_sprite->_castId.asString().c_str());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Score::updateSprites(RenderMode mode, bool withClean) {
|
|
if (_window->_newMovieStarted)
|
|
mode = kRenderForceUpdate;
|
|
|
|
debugC(5, kDebugImages, "Score::updateSprites(): starting render cycle, mode %d", mode);
|
|
|
|
_movie->_videoPlayback = false;
|
|
|
|
for (uint16 i = 0; i < _channels.size(); i++) {
|
|
Channel *channel = _channels[i];
|
|
Sprite *currentSprite = channel->_sprite;
|
|
Sprite *nextSprite = _currentFrame->_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) {
|
|
bool invalidCastMember = currentSprite && currentSprite->_spriteType == kCastMemberSprite && currentSprite->_cast == nullptr;
|
|
if (currentSprite && !invalidCastMember && !currentSprite->_trails)
|
|
_window->addDirtyRect(channel->getBbox());
|
|
|
|
if (currentSprite && currentSprite->_cast && currentSprite->_cast->_erase) {
|
|
currentSprite->_cast->_erase = false;
|
|
_movie->eraseCastMember(currentSprite->_castId);
|
|
|
|
currentSprite->setCast(currentSprite->_castId);
|
|
nextSprite->setCast(nextSprite->_castId);
|
|
}
|
|
|
|
// Only clean out the channel if we're moving to a different frame
|
|
if (withClean)
|
|
channel->setClean(nextSprite);
|
|
invalidCastMember = currentSprite ? (currentSprite->_spriteType == kCastMemberSprite && currentSprite->_cast == nullptr) : false;
|
|
// Check again to see if a video has just been started by setClean.
|
|
if (channel->isActiveVideo())
|
|
_movie->_videoPlayback = true;
|
|
|
|
if (!invalidCastMember)
|
|
_window->addDirtyRect(channel->getBbox());
|
|
|
|
if (currentSprite) {
|
|
Common::Rect bbox = channel->getBbox();
|
|
debugC(5, kDebugImages,
|
|
"Score::updateSprites(): CH: %-3d castId: %s invalid: %d [ink: %d, puppet: %d, moveable: %d, trails: %d, visible: %d] [bbox: %d,%d,%d,%d] [type: %d fg: %d bg: %d] [script: %s]",
|
|
i, currentSprite->_castId.asString().c_str(), invalidCastMember,
|
|
currentSprite->_ink, currentSprite->_puppet, currentSprite->_moveable,
|
|
currentSprite->_trails, channel->_visible,
|
|
PRINT_RECT(bbox), currentSprite->_spriteType, currentSprite->_foreColor, currentSprite->_backColor,
|
|
currentSprite->_scriptId.asString().c_str());
|
|
} else {
|
|
debugC(5, kDebugImages, "Score::updateSprites(): CH: %-3d: No sprite", i);
|
|
}
|
|
} else {
|
|
channel->setClean(nextSprite, 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(RenderMode mode) {
|
|
if (_puppetPalette)
|
|
return false;
|
|
|
|
// Skip this if we don't have a palette instruction
|
|
CastMemberID currentPalette = _currentFrame->_mainChannels.palette.paletteId;
|
|
if (currentPalette.isNull())
|
|
return false;
|
|
|
|
if (!_currentFrame->_mainChannels.palette.colorCycling &&
|
|
!_currentFrame->_mainChannels.palette.overTime) {
|
|
|
|
int frameRate = CLIP<int>(_currentFrame->_mainChannels.palette.speed, 1, 30);
|
|
|
|
if (debugChannelSet(-1, kDebugFast))
|
|
frameRate = 30;
|
|
|
|
if (g_director->_fpsLimit)
|
|
frameRate = MIN((int)g_director->_fpsLimit, frameRate);
|
|
|
|
int frameDelay = 1000 / 60;
|
|
int fadeFrames = kFadeColorFrames[frameRate - 1];
|
|
if (_vm->getVersion() >= 500)
|
|
fadeFrames = kFadeColorFramesD5[frameRate - 1];
|
|
byte calcPal[768];
|
|
|
|
// 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(currentPalette);
|
|
if (!destPal) {
|
|
warning("Unable to fetch palette %s", currentPalette.asString().c_str());
|
|
return false;
|
|
}
|
|
|
|
if (_currentFrame->_mainChannels.palette.normal) {
|
|
// If the target palette ID is the same as the previous palette ID,
|
|
// a normal fade is a no-op.
|
|
if (_currentFrame->_mainChannels.palette.paletteId == g_director->_lastPalette) {
|
|
return false;
|
|
}
|
|
|
|
// For fade palette transitions, the whole fade happens with
|
|
// the previous frame's layout.
|
|
debugC(2, kDebugImages, "Score::renderPrePaletteCycle(): fading palette to %s over %d frames", currentPalette.asString().c_str(), fadeFrames);
|
|
for (int i = 0; i < fadeFrames; i++) {
|
|
uint32 startTime = g_system->getMillis();
|
|
lerpPalette(
|
|
calcPal,
|
|
_paletteSnapshotBuffer, 256,
|
|
destPal->palette, destPal->length,
|
|
i + 1,
|
|
fadeFrames
|
|
);
|
|
g_director->setPalette(calcPal, 256);
|
|
g_director->draw();
|
|
if (_activeFade) {
|
|
_activeFade = _soundManager->fadeChannels();
|
|
}
|
|
// On click, stop loop and reset palette
|
|
if (_vm->processEvents(true)) {
|
|
debugC(2, kDebugImages, "Score::renderPrePaletteCycle(): interrupted, setting palette to %s", currentPalette.asString().c_str());
|
|
g_director->setPalette(currentPalette);
|
|
return true;
|
|
}
|
|
uint32 endTime = g_system->getMillis();
|
|
int diff = (int)frameDelay - (int)(endTime - startTime);
|
|
g_director->delayMillis(MAX(0, diff));
|
|
}
|
|
|
|
} else {
|
|
// For fade to black and fade to white palette transitions,
|
|
// the first half happens with the previous frame's layout.
|
|
|
|
const byte *fadePal = nullptr;
|
|
if (_currentFrame->_mainChannels.palette.fadeToBlack) {
|
|
// Fade everything except color index 0 to black
|
|
debugC(2, kDebugImages, "Score::renderPrePaletteCycle(): fading palette to black over %d frames", fadeFrames);
|
|
fadePal = kBlackPalette;
|
|
} else if (_currentFrame->_mainChannels.palette.fadeToWhite) {
|
|
// Fade everything except color index 255 to white
|
|
debugC(2, kDebugImages, "Score::renderPrePaletteCycle(): fading palette to white over %d frames", fadeFrames);
|
|
fadePal = kWhitePalette;
|
|
} else {
|
|
// Shouldn't reach here
|
|
return false;
|
|
}
|
|
|
|
for (int i = 0; i < fadeFrames; i++) {
|
|
uint32 startTime = g_system->getMillis();
|
|
lerpPalette(
|
|
calcPal,
|
|
_paletteSnapshotBuffer, 256,
|
|
fadePal, 256,
|
|
i + 1,
|
|
fadeFrames
|
|
);
|
|
g_director->setPalette(calcPal, 256);
|
|
g_director->draw();
|
|
if (_activeFade) {
|
|
_activeFade = _soundManager->fadeChannels();
|
|
}
|
|
// On click, stop loop and reset palette
|
|
if (_vm->processEvents(true)) {
|
|
debugC(2, kDebugImages, "Score::renderPrePaletteCycle(): interrupted, setting palette to %s", currentPalette.asString().c_str());
|
|
g_director->setPalette(currentPalette);
|
|
return true;
|
|
}
|
|
uint32 endTime = g_system->getMillis();
|
|
int diff = (int)frameDelay - (int)(endTime - startTime);
|
|
g_director->delayMillis(MAX(0, diff));
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void Score::setLastPalette() {
|
|
if (_puppetPalette)
|
|
return;
|
|
|
|
bool isCachedPalette = false;
|
|
CastMemberID currentPalette = _currentFrame->_mainChannels.palette.paletteId;
|
|
// Director allows you to use palette IDs for cast members
|
|
// that have long since been erased. Check all of them.
|
|
if (!g_director->hasPalette(currentPalette))
|
|
currentPalette = CastMemberID();
|
|
// Palette not specified in the frame
|
|
if (currentPalette.isNull()) {
|
|
// Use the score cached palette ID
|
|
isCachedPalette = true;
|
|
currentPalette = _currentFrame->_mainChannels.scoreCachedPaletteId;
|
|
if (!g_director->hasPalette(currentPalette))
|
|
currentPalette = CastMemberID();
|
|
// The cached ID is created before the cast gets loaded; if it's zero,
|
|
// this corresponds to the movie default palette.
|
|
if (currentPalette.isNull()) {
|
|
currentPalette = g_director->getCurrentMovie()->_defaultPalette;
|
|
}
|
|
// If for whatever reason this doesn't resolve, abort.
|
|
if (currentPalette.isNull())
|
|
return;
|
|
}
|
|
|
|
// If the palette is defined in the frame and doesn't match
|
|
// the current one, set it
|
|
bool paletteChanged = (currentPalette != g_director->_lastPalette) && (!currentPalette.isNull());
|
|
if (paletteChanged) {
|
|
debugC(2, kDebugImages, "Score::setLastPalette(): palette changed to %s, from %s", currentPalette.asString().c_str(), isCachedPalette ? "cache" :"frame");
|
|
g_director->_lastPalette = currentPalette;
|
|
_paletteTransitionIndex = 0;
|
|
|
|
// Switch to a new palette immediately if:
|
|
// - this is color cycling mode, or
|
|
// - the cached palette ID is different (i.e. we jumped in the score)
|
|
if (_currentFrame->_mainChannels.palette.colorCycling || isCachedPalette)
|
|
g_director->setPalette(g_director->_lastPalette);
|
|
}
|
|
|
|
}
|
|
|
|
bool Score::isPaletteColorCycling() {
|
|
return _currentFrame->_mainChannels.palette.colorCycling;
|
|
}
|
|
|
|
void Score::renderPaletteCycle(RenderMode mode) {
|
|
if (_puppetPalette)
|
|
return;
|
|
|
|
// If the palette is defined in the frame and doesn't match
|
|
// the current one, set it
|
|
CastMemberID currentPalette = _currentFrame->_mainChannels.palette.paletteId;
|
|
if (currentPalette.isNull())
|
|
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 = _currentFrame->_mainChannels.palette.speed;
|
|
if (speed == 0)
|
|
return;
|
|
|
|
// Apply the global FPS limit if required
|
|
if (g_director->_fpsLimit)
|
|
speed = MIN((int)g_director->_fpsLimit, speed);
|
|
|
|
if (debugChannelSet(-1, kDebugFast))
|
|
speed = 30;
|
|
|
|
// 30 (the maximum) is actually unbounded
|
|
int delay = speed == 30 ? 10 : 1000 / speed;
|
|
if (_currentFrame->_mainChannels.palette.colorCycling) {
|
|
// Cycle the colors of a chosen palette
|
|
int firstColor = _currentFrame->_mainChannels.palette.firstColor;
|
|
int lastColor = _currentFrame->_mainChannels.palette.lastColor;
|
|
|
|
if (_currentFrame->_mainChannels.palette.overTime) {
|
|
// Do a single color step in one frame transition
|
|
debugC(2, kDebugImages, "Score::renderPaletteCycle(): color cycle palette %s, from colors %d to %d, by 1 frame", currentPalette.asString().c_str(), firstColor, lastColor);
|
|
g_director->shiftPalette(firstColor, lastColor, false);
|
|
g_director->draw();
|
|
} else {
|
|
// Short circuit for few frames renderer
|
|
if (debugChannelSet(-1, kDebugFast)) {
|
|
g_director->setPalette(currentPalette);
|
|
return;
|
|
}
|
|
|
|
// Do a full color cycle in one frame transition
|
|
int steps = lastColor - firstColor + 1;
|
|
debugC(2, kDebugImages, "Score::renderPaletteCycle(): color cycle palette %s, from colors %d to %d, over %d steps %d times (delay: %d ms)", currentPalette.asString().c_str(), firstColor, lastColor, steps, _currentFrame->_mainChannels.palette.cycleCount, delay);
|
|
for (int i = 0; i < _currentFrame->_mainChannels.palette.cycleCount; i++) {
|
|
for (int j = 0; j < steps; j++) {
|
|
uint32 startTime = g_system->getMillis();
|
|
g_director->shiftPalette(firstColor, lastColor, false);
|
|
g_director->draw();
|
|
if (_activeFade) {
|
|
_activeFade = _soundManager->fadeChannels();
|
|
}
|
|
// On click, stop loop and reset palette
|
|
if (_vm->processEvents(true)) {
|
|
g_director->setPalette(currentPalette);
|
|
return;
|
|
}
|
|
uint32 endTime = g_system->getMillis();
|
|
int diff = (int)delay - (int)(endTime - startTime);
|
|
g_director->delayMillis(MAX(0, diff));
|
|
}
|
|
if (_currentFrame->_mainChannels.palette.autoReverse) {
|
|
for (int j = 0; j < steps; j++) {
|
|
uint32 startTime = g_system->getMillis();
|
|
g_director->shiftPalette(firstColor, lastColor, true);
|
|
g_director->draw();
|
|
if (_activeFade) {
|
|
_activeFade = _soundManager->fadeChannels();
|
|
}
|
|
// On click, stop loop and reset palette
|
|
if (_vm->processEvents(true)) {
|
|
g_director->setPalette(currentPalette);
|
|
return;
|
|
}
|
|
uint32 endTime = g_system->getMillis();
|
|
int diff = (int)delay - (int)(endTime - startTime);
|
|
g_director->delayMillis(MAX(0, diff));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Transition from the current palette to a new palette
|
|
PaletteV4 *destPal = g_director->getPalette(currentPalette);
|
|
if (!destPal) {
|
|
warning("Score::renderPaletteCycle(): no match for palette id %s", currentPalette.asString().c_str());
|
|
return;
|
|
}
|
|
int frameCount = _currentFrame->_mainChannels.palette.frameCount;
|
|
byte calcPal[768];
|
|
|
|
if (_currentFrame->_mainChannels.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);
|
|
debugC(2, kDebugImages, "Score::renderPaletteCycle(): fading palette to %s over %d frames", currentPalette.asString().c_str(), frameCount);
|
|
}
|
|
|
|
if (_currentFrame->_mainChannels.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;
|
|
|
|
const byte *fadePal = nullptr;
|
|
if (_currentFrame->_mainChannels.palette.fadeToBlack) {
|
|
// Fade everything except color index 0 to black
|
|
fadePal = kBlackPalette;
|
|
} else if (_currentFrame->_mainChannels.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)) {
|
|
debugC(2, kDebugImages, "Score::renderPaletteCycle(): setting palette to %s", currentPalette.asString().c_str());
|
|
g_director->setPalette(currentPalette);
|
|
return;
|
|
}
|
|
|
|
// Do a full cycle in one frame transition
|
|
// For normal mode, we've already faded the palette in renderPrePaletteCycle
|
|
if (!_currentFrame->_mainChannels.palette.normal) {
|
|
const byte *fadePal = nullptr;
|
|
if (_currentFrame->_mainChannels.palette.fadeToBlack) {
|
|
// Fade everything except color index 0 to black
|
|
fadePal = kBlackPalette;
|
|
} else if (_currentFrame->_mainChannels.palette.fadeToWhite) {
|
|
// Fade everything except color index 255 to white
|
|
fadePal = kWhitePalette;
|
|
} else {
|
|
// Shouldn't reach here
|
|
return;
|
|
}
|
|
int frameRate = CLIP<int>(_currentFrame->_mainChannels.palette.speed, 1, 30);
|
|
|
|
if (debugChannelSet(-1, kDebugFast))
|
|
frameRate = 30;
|
|
|
|
if (g_director->_fpsLimit)
|
|
frameRate = MIN((int)g_director->_fpsLimit, frameRate);
|
|
|
|
int frameDelay = 1000 / 60;
|
|
int fadeFrames = kFadeColorFrames[frameRate - 1];
|
|
if (_vm->getVersion() >= 500)
|
|
fadeFrames = kFadeColorFramesD5[frameRate - 1];
|
|
|
|
// Wait for a fixed time
|
|
g_director->setPalette(fadePal, 256);
|
|
g_director->draw();
|
|
for (int i = 0; i < kFadeColorWait; i++) {
|
|
uint32 startTime = g_system->getMillis();
|
|
if (_activeFade) {
|
|
_activeFade = _soundManager->fadeChannels();
|
|
}
|
|
// On click, stop loop and reset palette
|
|
if (_vm->processEvents(true)) {
|
|
debugC(2, kDebugImages, "Score::renderPaletteCycle(): interrupted, setting palette to %s", currentPalette.asString().c_str());
|
|
g_director->setPalette(currentPalette);
|
|
return;
|
|
}
|
|
uint32 endTime = g_system->getMillis();
|
|
int diff = (int)frameDelay - (int)(endTime - startTime);
|
|
g_director->delayMillis(MAX(0, diff));
|
|
}
|
|
|
|
debugC(2, kDebugImages, "Score::renderPaletteCycle(): fading palette to %s over %d frames", currentPalette.asString().c_str(), fadeFrames);
|
|
|
|
for (int i = 0; i < fadeFrames; i++) {
|
|
uint32 startTime = g_system->getMillis();
|
|
lerpPalette(
|
|
calcPal,
|
|
fadePal, 256,
|
|
destPal->palette, destPal->length,
|
|
i + 1,
|
|
fadeFrames
|
|
);
|
|
g_director->setPalette(calcPal, 256);
|
|
g_director->draw();
|
|
if (_activeFade) {
|
|
_activeFade = _soundManager->fadeChannels();
|
|
}
|
|
// On click, stop loop and reset palette
|
|
if (_vm->processEvents(true)) {
|
|
debugC(2, kDebugImages, "Score::renderPaletteCycle(): interrupted, setting palette to %s", currentPalette.asString().c_str());
|
|
g_director->setPalette(currentPalette);
|
|
return;
|
|
}
|
|
uint32 endTime = g_system->getMillis();
|
|
int diff = (int)frameDelay - (int)(endTime - startTime);
|
|
g_director->delayMillis(MAX(0, diff));
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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());
|
|
}
|
|
}
|
|
}
|
|
|
|
bool Score::checkShotSimilarity(const Graphics::Surface *oldSurface, const Graphics::Surface *newSurface) {
|
|
if (oldSurface->w != newSurface->w || oldSurface->h != newSurface->h || oldSurface->format != newSurface->format) {
|
|
warning("BUILDBOT: Score::checkShotSimilarity(): Dimensions or format do not match");
|
|
return false;
|
|
}
|
|
|
|
uint32 absolute_pixel_differences = 0;
|
|
uint32 different_pixel_count = 0;
|
|
uint32 total_pixel_count = oldSurface->w * oldSurface->h;
|
|
|
|
for (int y = 0; y < oldSurface->h; y++) {
|
|
const uint32 *oldPtr = (const uint32 *)oldSurface->getBasePtr(0, y);
|
|
const uint32 *newPtr = (const uint32 *)newSurface->getBasePtr(0, y);
|
|
|
|
for (int x = 0; x < oldSurface->w; x++) {
|
|
uint32 newColor = *newPtr++;
|
|
uint32 oldColor = *oldPtr++;
|
|
|
|
if (newColor != oldColor) {
|
|
absolute_pixel_differences++;
|
|
|
|
for (int c = 0; c < 4; c++) {
|
|
if (ABS<int32>((newColor & 0xFF) - (oldColor & 0xFF)) > kShotColorDiffThreshold) {
|
|
different_pixel_count++;
|
|
break;
|
|
}
|
|
|
|
newColor >>= 8;
|
|
oldColor >>= 8;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check 1: If two images are absolutely same, we don't need to check further
|
|
if (absolute_pixel_differences == 0) {
|
|
return true;
|
|
}
|
|
|
|
// Check 2: Images are different, but the difference can be small enough to be in threshold
|
|
Common::Rational difference_percentage = Common::Rational(different_pixel_count, total_pixel_count);
|
|
if (difference_percentage > kShotPercentPixelThreshold)
|
|
warning("BUILDBOT: Score::checkShotSimilarity(): Screenshot is %d%% different from previous one, threshold is %d percent", difference_percentage.getNumerator() * 100 / difference_percentage.getDenominator(), kShotPercentPixelThreshold);
|
|
|
|
return false;
|
|
}
|
|
|
|
void Score::screenShot() {
|
|
#ifndef USE_PNG
|
|
warning("Screenshot requested, but PNG support is not compiled in");
|
|
|
|
return;
|
|
#else
|
|
|
|
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(), Common::punycode_encodefilename(_movie->getMacName()).c_str());
|
|
Common::Path filename = dumpScriptName(prefix.c_str(), kMovieScript, g_director->_framesRan, "png");
|
|
|
|
const char *buildNumber = getenv("BUILD_NUMBER");
|
|
|
|
// If we are not inside of buildbot, we just dump it
|
|
if (buildNumber && ConfMan.hasKey("screenshotpath")) {
|
|
// The filename is in the form:
|
|
// ./dumps/theapartment/25/xn--Main Menu-zd0e-19.png
|
|
|
|
Common::Path buildDir(Common::String::format("%s/%s", ConfMan.get("screenshotpath").c_str(),
|
|
g_director->getTargetName().c_str()), '/');
|
|
|
|
// We run for the first time, let's check if we had the directory previously
|
|
if (_previousBuildBotBuild == -1) {
|
|
Common::FSNode dir(buildDir);
|
|
|
|
if (!dir.exists())
|
|
_previousBuildBotBuild = 0; // We will skip attempts to search screenshots
|
|
else
|
|
_previousBuildBotBuild = atoi(buildNumber) - 1;
|
|
}
|
|
|
|
int prevbuild = _previousBuildBotBuild;
|
|
|
|
// Now we try to find any previous dump
|
|
while (prevbuild > 0) {
|
|
filename = buildDir.join(Common::Path(Common::String::format("%d/%s-%d.png", prevbuild, prefix.c_str(), g_director->_framesRan), '/'));
|
|
|
|
Common::FSNode fs(filename);
|
|
|
|
if (fs.exists())
|
|
break;
|
|
|
|
prevbuild--;
|
|
}
|
|
|
|
// We found a previous screenshot. Let's compare it
|
|
if (prevbuild > 0) {
|
|
Common::FSNode fs(filename);
|
|
Image::PNGDecoder decoder;
|
|
Common::SeekableReadStream *stream = fs.createReadStream();
|
|
|
|
if (stream && decoder.loadStream(*stream)) {
|
|
if (checkShotSimilarity(decoder.getSurface(), newSurface)) {
|
|
warning("Screenshot is equal to previous one, skipping: %s", filename.toString(Common::Path::kNativeSeparator).c_str());
|
|
newSurface->free();
|
|
delete newSurface;
|
|
delete stream;
|
|
return;
|
|
}
|
|
} else {
|
|
warning("Error loading previous screenshot %s", filename.toString(Common::Path::kNativeSeparator).c_str());
|
|
}
|
|
|
|
delete stream;
|
|
}
|
|
|
|
// We are here because we either have nothing to compare with or
|
|
// the screenshot was different from the previous one.
|
|
//
|
|
// Regenerate file name with the correct build number
|
|
filename = buildDir.join(Common::Path(Common::String::format("%s/%s-%d.png", buildNumber, prefix.c_str(), g_director->_framesRan), '/'));
|
|
}
|
|
|
|
Common::DumpFile screenshotFile;
|
|
if (screenshotFile.open(filename, true)) {
|
|
debug("Dumping screenshot to %s", filename.toString(Common::Path::kNativeSeparator).c_str());
|
|
|
|
Image::writePNG(screenshotFile, *newSurface);
|
|
} else {
|
|
warning("Cannot write screenshot to %s", filename.toString(Common::Path::kNativeSeparator).c_str());
|
|
}
|
|
|
|
newSurface->free();
|
|
delete newSurface;
|
|
|
|
#endif // USE_PNG
|
|
}
|
|
|
|
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;
|
|
Common::List<Channel *> appendix;
|
|
|
|
for (uint i = 0; i < _channels.size(); i++) {
|
|
if (!_channels[i]->isEmpty() && !r.findIntersectingRect(_channels[i]->getBbox()).isEmpty()) {
|
|
// Editable text sprites will (more or less) always be rendered in front of other sprites,
|
|
// regardless of their order in the channel list.
|
|
if (_channels[i]->getEditable()) {
|
|
appendix.push_back(_channels[i]);
|
|
} else {
|
|
intersections.push_back(_channels[i]);
|
|
}
|
|
}
|
|
}
|
|
for (auto &ch : appendix) {
|
|
intersections.push_back(ch);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
bool Score::refreshPointersForCastMemberID(CastMemberID id) {
|
|
// FIXME: This can be removed once Sprite is refactored to not
|
|
// keep a pointer to a CastMember.
|
|
bool hit = false;
|
|
for (auto &it : _channels) {
|
|
if (it->_sprite->_castId == id) {
|
|
it->_sprite->_cast = nullptr;
|
|
it->setCast(id);
|
|
it->_dirty = true;
|
|
hit = true;
|
|
}
|
|
}
|
|
|
|
for (auto &it : _currentFrame->_sprites) {
|
|
if (it->_castId == id) {
|
|
it->_cast = nullptr;
|
|
it->setCast(id);
|
|
hit = true;
|
|
}
|
|
}
|
|
return hit;
|
|
}
|
|
|
|
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", _curFrameNumber, id);
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
Sprite *Score::getOriginalSpriteById(uint16 id) {
|
|
if (id < _currentFrame->_sprites.size())
|
|
return _currentFrame->_sprites[id];
|
|
warning("Score::getOriginalSpriteById(%d): out of bounds, >= %d", id, _currentFrame->_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(bool puppetOnly) {
|
|
DirectorSound *sound = _window->getSoundManager();
|
|
debugC(5, kDebugSound, "Score::playSoundChannel(): Sound1: %s puppet: %d type: %d, volume: %d, Sound2: %s puppet: %d, type: %d, volume: %d",
|
|
_currentFrame->_mainChannels.sound1.asString().c_str(), sound->isChannelPuppet(1), _currentFrame->_mainChannels.soundType1, sound->getChannelVolume(1),
|
|
_currentFrame->_mainChannels.sound2.asString().c_str(), sound->isChannelPuppet(2), _currentFrame->_mainChannels.soundType2, sound->getChannelVolume(2));
|
|
|
|
if (sound->isChannelPuppet(1)) {
|
|
sound->playPuppetSound(1);
|
|
} else if (!puppetOnly) {
|
|
if (_currentFrame->_mainChannels.soundType1 >= kMinSampledMenu && _currentFrame->_mainChannels.soundType1 <= kMaxSampledMenu) {
|
|
sound->playExternalSound(_currentFrame->_mainChannels.soundType1, _currentFrame->_mainChannels.sound1.member, 1);
|
|
} else {
|
|
sound->playCastMember(_currentFrame->_mainChannels.sound1, 1);
|
|
}
|
|
}
|
|
|
|
if (sound->isChannelPuppet(2)) {
|
|
sound->playPuppetSound(2);
|
|
} else if (!puppetOnly) {
|
|
if (_currentFrame->_mainChannels.soundType2 >= kMinSampledMenu && _currentFrame->_mainChannels.soundType2 <= kMaxSampledMenu) {
|
|
sound->playExternalSound(_currentFrame->_mainChannels.soundType2, _currentFrame->_mainChannels.sound2.member, 2);
|
|
} else {
|
|
sound->playCastMember(_currentFrame->_mainChannels.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");
|
|
|
|
// Setup our streams for frames processing
|
|
uint dataSize = stream.size();
|
|
byte *data = (byte *)malloc(dataSize);
|
|
stream.read(data, dataSize);
|
|
|
|
_framesStream = new Common::MemoryReadStreamEndian(data, dataSize, stream.isBE(), DisposeAfterUse::YES);
|
|
|
|
if (debugChannelSet(8, kDebugLoading)) {
|
|
_framesStream->hexdump(_framesStream->size());
|
|
}
|
|
|
|
_framesStreamSize = _framesStream->readUint32();
|
|
|
|
if (version < kFileVer400) {
|
|
_numChannelsDisplayed = 30;
|
|
} else if (version >= kFileVer400 && version < kFileVer600) {
|
|
uint32 frame1Offset = _framesStream->readUint32();
|
|
/* uint32 numOfFrames = */ _framesStream->readUint32();
|
|
_framesVersion = _framesStream->readUint16();
|
|
uint16 spriteRecordSize = _framesStream->readUint16();
|
|
_numChannels = _framesStream->readUint16();
|
|
|
|
if (_framesVersion > 13) {
|
|
_numChannelsDisplayed = _framesStream->readUint16();
|
|
} else {
|
|
if (_framesVersion <= 7) // Director5
|
|
_numChannelsDisplayed = 48;
|
|
else
|
|
_numChannelsDisplayed = 120; // D6
|
|
|
|
_framesStream->readUint16(); // Skip
|
|
}
|
|
|
|
debugC(1, kDebugLoading, "Score::loadFrames(): frame1Offset: 0x%x, version: %d, spriteRecordSize: 0x%x, numChannels: %d, numChannelsDisplayed: %d",
|
|
frame1Offset, _framesVersion, spriteRecordSize, _numChannels, _numChannelsDisplayed);
|
|
// Unknown, some bytes - constant (refer to contuinity).
|
|
} else {
|
|
error("STUB: Score::loadFrames(): score not yet supported for version %d", version);
|
|
}
|
|
|
|
// partically by channels, hence we keep it and read the score from left to right
|
|
// TODO Merge it with shared cast
|
|
_currentFrame = new Frame(this, _numChannelsDisplayed);
|
|
|
|
_currentTempo = 0;
|
|
_currentPaletteId = CastMemberID(0, 0);
|
|
|
|
// Prepare frameOffsets
|
|
_version = version;
|
|
_firstFramePosition = _framesStream->pos();
|
|
|
|
// Pre-computing number of frames, as sometimes the frameNumber in stream mismatches
|
|
debugC(1, kDebugLoading, "Score::loadFrames(): Precomputing total number of frames! First frame pos: %d, framesstreamsizeL %d",
|
|
_firstFramePosition, _framesStreamSize);
|
|
|
|
// Calculate number of frames and their positions
|
|
// numOfFrames in the header is often incorrect
|
|
for (_numFrames = 1; loadFrame(_numFrames, false); _numFrames++) {
|
|
_scoreCache.push_back(new Frame(*_currentFrame));
|
|
}
|
|
|
|
debugC(1, kDebugLoading, "Score::loadFrames(): Calculated, total number of frames %d!", _numFrames);
|
|
|
|
_currentFrame->reset();
|
|
|
|
loadFrame(1, true);
|
|
|
|
debugC(1, kDebugLoading, "Score::loadFrames(): Number of frames: %d, framesStreamSize: %d", _numFrames, _framesStreamSize);
|
|
}
|
|
|
|
bool Score::loadFrame(int frameNum, bool loadCast) {
|
|
debugC(7, kDebugLoading, "****** Frame request %d, current pos: %" PRId64 ", current frame number: %d", frameNum, _framesStream->pos(), _curFrameNumber);
|
|
|
|
int sourceFrame = _curFrameNumber;
|
|
int targetFrame = frameNum;
|
|
|
|
if (frameNum <= (int)_curFrameNumber) {
|
|
debugC(7, kDebugLoading, "****** Resetting frame %d to start %" PRId64, sourceFrame, _framesStream->pos());
|
|
// If we are going back, we need to rebuild frames from start
|
|
_currentFrame->reset();
|
|
sourceFrame = 0;
|
|
|
|
// Reset position to start
|
|
_framesStream->seek(_firstFramePosition);
|
|
|
|
// Reset sprite contents
|
|
for (auto &it : _currentFrame->_sprites)
|
|
it->reset();
|
|
}
|
|
|
|
debugC(7, kDebugLoading, "****** Source frame %d to Destination frame %d, current offset %" PRId64, sourceFrame, targetFrame, _framesStream->pos());
|
|
|
|
while (sourceFrame < targetFrame - 1 && readOneFrame()) {
|
|
sourceFrame++;
|
|
}
|
|
|
|
// Finally read the target frame!
|
|
bool isFrameRead = readOneFrame();
|
|
if (!isFrameRead)
|
|
return false;
|
|
|
|
// We have read the frame, now update current frame number
|
|
_curFrameNumber = targetFrame;
|
|
|
|
if (loadCast) {
|
|
// Load frame cast
|
|
setSpriteCasts();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Score::readOneFrame() {
|
|
uint16 channelSize;
|
|
uint16 channelOffset;
|
|
|
|
if (_framesStream->pos() >= _framesStreamSize || _framesStream->eos())
|
|
return false;
|
|
|
|
uint16 frameSize = _framesStream->readUint16();
|
|
debugC(4, kDebugLoading, "pos: %" PRId64 " frameSize: %d (0x%x) streamSize: %d", _framesStream->pos() - 2, frameSize, frameSize, _framesStreamSize);
|
|
assert(frameSize < _framesStreamSize);
|
|
|
|
debugC(3, kDebugLoading, "++++++++++ score load frame %d (frameSize %d) saveOffset", _curFrameNumber, frameSize);
|
|
if (debugChannelSet(8, kDebugLoading)) {
|
|
_framesStream->hexdump(MAX(0, frameSize - 2));
|
|
}
|
|
if (frameSize > 0) {
|
|
frameSize -= 2;
|
|
|
|
while (frameSize != 0) {
|
|
|
|
if (_vm->getVersion() < 400) {
|
|
channelSize = _framesStream->readByte() * 2;
|
|
channelOffset = _framesStream->readByte() * 2;
|
|
frameSize -= channelSize + 2;
|
|
} else {
|
|
channelSize = _framesStream->readUint16();
|
|
channelOffset = _framesStream->readUint16();
|
|
frameSize -= channelSize + 4;
|
|
}
|
|
|
|
_currentFrame->readChannel(*_framesStream, channelOffset, channelSize, _version);
|
|
}
|
|
|
|
if (debugChannelSet(9, kDebugLoading)) {
|
|
debugC(9, kDebugLoading, "%s", _currentFrame->formatChannelInfo().c_str());
|
|
}
|
|
|
|
debugC(8, kDebugLoading, "Score::readOneFrame(): Frame %d actionId: %s", _curFrameNumber, _currentFrame->_mainChannels.actionId.asString().c_str());
|
|
return true;
|
|
} else {
|
|
warning("Score::readOneFrame(): Zero sized frame!? exiting loop until we know what to do with the tags that follow.");
|
|
}
|
|
|
|
return false; // Error in loading frame
|
|
}
|
|
|
|
Frame *Score::getFrameData(int frameNum){
|
|
// This function is for previewing selected frame,
|
|
// It doesn't make any changes to current render state
|
|
// In case of any problem, it returns nullptr
|
|
// Be sure to delete this frame after use
|
|
|
|
// Backup variables
|
|
int tempFrameNumber = _curFrameNumber;
|
|
|
|
bool isFrameRead = loadFrame(frameNum, false);
|
|
Frame *tempFrame = _currentFrame;
|
|
|
|
_currentFrame = new Frame(this, _numChannelsDisplayed);
|
|
loadFrame(frameNum, true);
|
|
Frame *frame = _currentFrame;
|
|
|
|
_currentFrame = tempFrame;
|
|
_curFrameNumber = tempFrameNumber;
|
|
|
|
if (isFrameRead) {
|
|
return frame;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void Score::setSpriteCasts() {
|
|
// Update sprite cache of cast pointers/info
|
|
for (uint16 j = 0; j < _currentFrame->_sprites.size(); j++) {
|
|
_currentFrame->_sprites[j]->setCast(_currentFrame->_sprites[j]->_castId, !_currentFrame->_sprites[j]->_stretch);
|
|
|
|
debugC(8, kDebugLoading, "Score::setSpriteCasts(): Frame: 0 Channel: %d castId: %s type: %d (%s)",
|
|
j, _currentFrame->_sprites[j]->_castId.asString().c_str(), _currentFrame->_sprites[j]->_spriteType,
|
|
spriteType2str(_currentFrame->_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;
|
|
}
|
|
|
|
debugC(2, kDebugLoading, "****** Loading labels");
|
|
for (auto &j : *_labels) {
|
|
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;
|
|
}
|
|
|
|
for (auto &j : _actions) {
|
|
if (!j._value.empty()) {
|
|
if (ConfMan.getBool("dump_scripts"))
|
|
_movie->getCast()->dumpScript(j._value.c_str(), kScoreScript, j._key);
|
|
|
|
_movie->getMainLingoArch()->addCode(j._value, kScoreScript, j._key, nullptr, kLPPTrimGarbage);
|
|
|
|
processImmediateFrameScript(j._value, j._key);
|
|
}
|
|
}
|
|
}
|
|
|
|
Common::String Score::formatChannelInfo() {
|
|
Frame &frame = *_currentFrame;
|
|
Common::String result;
|
|
CastMemberID defaultPalette = g_director->getCurrentMovie()->_defaultPalette;
|
|
result += Common::String::format("TMPO: tempo: %d, skipFrameFlag: %d, blend: %d, currentFPS: %d\n",
|
|
frame._mainChannels.tempo, frame._mainChannels.skipFrameFlag, frame._mainChannels.blend, _currentFrameRate);
|
|
if (!frame._mainChannels.palette.paletteId.isNull()) {
|
|
result += Common::String::format("PAL: paletteId: %s, firstColor: %d, lastColor: %d, flags: %d, cycleCount: %d, speed: %d, frameCount: %d, fade: %d, delay: %d, style: %d, currentId: %s, defaultId: %s\n",
|
|
frame._mainChannels.palette.paletteId.asString().c_str(), frame._mainChannels.palette.firstColor, frame._mainChannels.palette.lastColor, frame._mainChannels.palette.flags,
|
|
frame._mainChannels.palette.cycleCount, frame._mainChannels.palette.speed, frame._mainChannels.palette.frameCount,
|
|
frame._mainChannels.palette.fade, frame._mainChannels.palette.delay, frame._mainChannels.palette.style, g_director->_lastPalette.asString().c_str(), defaultPalette.asString().c_str());
|
|
} else {
|
|
result += Common::String::format("PAL: paletteId: 000, currentId: %s, defaultId: %s\n", g_director->_lastPalette.asString().c_str(), defaultPalette.asString().c_str());
|
|
}
|
|
result += Common::String::format("TRAN: transType: %d, transDuration: %d, transChunkSize: %d\n",
|
|
frame._mainChannels.transType, frame._mainChannels.transDuration, frame._mainChannels.transChunkSize);
|
|
result += Common::String::format("SND: 1 sound1: %d, soundType1: %d\n", frame._mainChannels.sound1.member, frame._mainChannels.soundType1);
|
|
result += Common::String::format("SND: 2 sound2: %d, soundType2: %d\n", frame._mainChannels.sound2.member, frame._mainChannels.soundType2);
|
|
result += Common::String::format("LSCR: actionId: %s\n", frame._mainChannels.actionId.asString().c_str());
|
|
|
|
for (int i = 0; i < frame._numChannels; i++) {
|
|
Channel &channel = *_channels[i + 1];
|
|
Sprite &sprite = *channel._sprite;
|
|
Common::Point position = channel.getPosition();
|
|
if (sprite._castId.member) {
|
|
result += Common::String::format("CH: %-3d castId: %s, visible: %d, [inkData: 0x%02x [ink: %d, trails: %d, stretch: %d, line: %d], %dx%d@%d,%d type: %d (%s) fg: %d bg: %d], script: %s, colorcode: 0x%x, blendAmount: 0x%x, unk3: 0x%x, constraint: %d, puppet: %d, moveable: %d, movieRate: %f, movieTime: %d (%f), filmLoopFrame: %d\n",
|
|
i + 1, sprite._castId.asString().c_str(), channel._visible, sprite._inkData,
|
|
sprite._ink, sprite._trails, sprite._stretch, sprite._thickness,
|
|
channel.getWidth(), channel.getHeight(), position.x, position.y,
|
|
sprite._spriteType, spriteType2str(sprite._spriteType), sprite._foreColor, sprite._backColor,
|
|
sprite._scriptId.asString().c_str(), sprite._colorcode, sprite._blendAmount, sprite._unk3,
|
|
channel._constraint, sprite._puppet, sprite._moveable, channel._movieRate, channel._movieTime, (float)(channel._movieTime/60.0f), channel._filmLoopFrame);
|
|
} else {
|
|
result += Common::String::format("CH: %-3d castId: 000\n", i + 1);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
} // End of namespace Director
|