scummvm/engines/mtropolis/elements.cpp
Matthew Duggan 704d1d0211 MTROPOLIS: Fix crash - correct movie chunk offset
Fixes a typo introduced in PR#5378 which used the wrong value as the chunk
offset.
2023-11-25 09:37:51 +11:00

2633 lines
89 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 "video/video_decoder.h"
#include "video/qt_decoder.h"
#include "video/avi_decoder.h"
#include "common/file.h"
#include "common/substream.h"
#include "graphics/macgui/macfontmanager.h"
#include "graphics/fontman.h"
#include "graphics/font.h"
#include "graphics/managed_surface.h"
#include "mtropolis/assets.h"
#include "mtropolis/audio_player.h"
#include "mtropolis/elements.h"
#include "mtropolis/element_factory.h"
#include "mtropolis/miniscript.h"
#include "mtropolis/render.h"
namespace MTropolis {
GraphicElement::GraphicElement() : _cacheBitmap(false) {
}
GraphicElement::~GraphicElement() {
}
bool GraphicElement::load(ElementLoaderContext &context, const Data::GraphicElement &data) {
if (!loadCommon(data.name, data.guid, data.rect1, data.elementFlags, data.layer, data.streamLocator, data.sectionID))
return false;
_cacheBitmap = ((data.elementFlags & Data::ElementFlags::kCacheBitmap) != 0);
return true;
}
bool GraphicElement::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
if (attrib == "cache") {
result.setBool(_cacheBitmap);
return true;
}
return VisualElement::readAttribute(thread, result, attrib);
}
MiniscriptInstructionOutcome GraphicElement::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) {
if (attrib == "cache") {
DynamicValueWriteBoolHelper::create(&_cacheBitmap, result);
return kMiniscriptInstructionOutcomeContinue;
}
return VisualElement::writeRefAttribute(thread, result, attrib);
}
void GraphicElement::render(Window *window) {
bool haveEffect = (_bottomRightBevelShading != 0 || _topLeftBevelShading != 0 || _interiorShading != 0);
if (!haveEffect) {
if (_renderProps.getInkMode() == VisualElementRenderProperties::kInkModeDefault || _renderProps.getInkMode() == VisualElementRenderProperties::kInkModeInvisible || _rect.isEmpty()) {
// Not rendered at all
_mask.reset();
return;
}
}
if (!_visible)
return;
const bool needsMask = (_renderProps.getShape() != VisualElementRenderProperties::kShapeRect);
bool needsMaskRedraw = _renderProps.isDirty();
uint16 width = _rect.width();
uint16 height = _rect.height();
if (needsMask) {
if (!_mask || _mask->w != width || _mask->h != height) {
_mask.reset();
_mask.reset(new Graphics::ManagedSurface());
_mask->create(_rect.width(), _rect.height(), Graphics::PixelFormat::createFormatCLUT8());
needsMaskRedraw = true;
}
} else {
_mask.reset();
}
if (needsMaskRedraw) {
Common::Array<Common::Point> starPoints;
const Common::Array<Common::Point> *polyPoints = nullptr;
VisualElementRenderProperties::Shape shape = _renderProps.getShape();
if (shape == VisualElementRenderProperties::kShapeStar) {
starPoints.resize(10);
starPoints[0] = Common::Point(width / 2, 0);
starPoints[1] = Common::Point(width * 2 / 3, height / 3);
starPoints[2] = Common::Point(width, height / 3);
starPoints[3] = Common::Point(width * 3 / 4, height / 2);
starPoints[4] = Common::Point(width, height);
starPoints[5] = Common::Point(width / 2, height * 2 / 3);
starPoints[6] = Common::Point(0, height);
starPoints[7] = Common::Point(width / 4, height / 2);
starPoints[8] = Common::Point(0, height / 3);
starPoints[9] = Common::Point(width / 3, height / 3);
polyPoints = &starPoints;
shape = VisualElementRenderProperties::kShapePolygon;
} else if (shape == VisualElementRenderProperties::kShapePolygon) {
polyPoints = &_renderProps.getPolyPoints();
}
// Notes for future:
// Rounded rect corner arc size is fixed at 13x13 unless the graphic is smaller.
// TODO: Overhaul this again to be more accurate, it was designed to work "OpenGL-style"
// where a point exactly on an edge would be excluded on a right/bottom edge to ensure
// that the canvas puzzle in Obsidian worked correctly, but it turns out that the canvas
// puzzle only uses polys for the mouse collision and uses a special shape ID for the
// triangles. This most likely is supposed to work "QuickDraw style" where the pixel
// coordinates of the poly points are always included.
//
// Maybe it doesn't matter, but if we find a game in the future with polygons, and
// there are 1-pixel gaps or something.... that's why. And that's what needs to be fixed.
//
// In fact, because of that, we should probably just use ScummVM's draw routines instead
// of this code here. Sigh.
if (shape == VisualElementRenderProperties::kShapePolygon && polyPoints->size() >= 3) {
_mask->clear(0);
Common::Point firstPoint = (*polyPoints)[0];
for (uint polyStart = 1; polyStart < polyPoints->size() - 1; polyStart++) {
Common::Point points[3];
points[0] = firstPoint;
points[1] = (*polyPoints)[polyStart];
points[2] = (*polyPoints)[polyStart + 1];
// Sort poly points into height ascending order
for (int sortStart = 0; sortStart < 2; sortStart++) {
Common::Point *thisPoint = &points[sortStart];
Common::Point *lowestY = thisPoint;
for (int candidateIndex = sortStart + 1; candidateIndex < 3; candidateIndex++) {
Common::Point *candidate = &points[candidateIndex];
if (candidate->y < lowestY->y)
lowestY = candidate;
}
if (lowestY != thisPoint) {
Common::Point temp = *thisPoint;
*thisPoint = *lowestY;
*lowestY = temp;
}
}
if (points[0].y == points[2].y)
continue; // Degenerate triangle
// Bin into 2 sets
Common::Point *triPoints[2][3] = {{&points[0], &points[1], &points[2]},
{&points[2], &points[1], &points[0]}};
int32 yRanges[2][2] = {{points[0].y, points[1].y},
{points[1].y, points[2].y}};
for (int half = 0; half < 2; half++) {
Common::Point *commonPoint = triPoints[half][0];
Common::Point *leftVert = triPoints[half][1];
Common::Point *rightVert = triPoints[half][2];
if (leftVert->x > rightVert->x) {
Common::Point *temp = leftVert;
leftVert = rightVert;
rightVert = temp;
}
int32 minY = yRanges[half][0];
if (minY < 0)
minY = 0;
int32 maxY = yRanges[half][1];
if (maxY > static_cast<int32>(height))
maxY = height;
// Compute scanline rays
// In theory we'd want rays that are x=y*scale+const
// But since we're operating on pixel center space, what we actually want is
// x=(y+0.5)*scale+const-0.5
int32 rayScaleNum[2];
int32 rayConstNum[2];
int32 rayDenom[2];
int32 verts[2][2] = {{leftVert->x, leftVert->y},
{rightVert->x, rightVert->y}};
for (int ray = 0; ray < 2; ray++) {
int32 x0 = verts[ray][0];
int32 y0 = verts[ray][1];
int32 x1 = commonPoint->x;
int32 y1 = commonPoint->y;
// Compute the base function for:
// x0=y0*scale+const
// x1=y1*scale+const
// Where a=x0, b=y0, c=x1, d=y1, x=scale, y=const
rayScaleNum[ray] = x0 - x1;
rayConstNum[ray] = y0 * x1 - x0 * y1;
rayDenom[ray] = y0 - y1;
// Half-pixel nudge y: x=(y+1/2)*scale+const-1/2
// x = (y*scale + 1/2*scale + const)/denom - 1/2
// x = (y*2*scale + scale + 2*const - denom)/2*denom
rayConstNum[ray] = 2 * rayConstNum[ray] + rayScaleNum[ray] - rayDenom[ray];
rayScaleNum[ray] *= 2;
rayDenom[ray] *= 2;
// Ensure the denominator is positive
if (rayDenom[ray] < 0) {
rayDenom[ray] = -rayDenom[ray];
rayScaleNum[ray] = -rayScaleNum[ray];
rayConstNum[ray] = -rayConstNum[ray];
}
}
for (int32 y = minY; y < maxY; y++) {
int32 xSpan[2];
for (int32 ray = 0; ray < 2; ray++) {
int32 xNum = y * rayScaleNum[ray] + rayConstNum[ray];
// Round up. If x < 0 then the divide will be towards zero (up)
if (xNum >= 0)
xNum += rayDenom[ray] - 1;
int32 resolved = xNum / rayDenom[ray];
if (resolved < 0)
resolved = 0;
else if (resolved > width)
resolved = width;
xSpan[ray] = resolved;
}
if (xSpan[1] < xSpan[0]) {
int32 temp = xSpan[1];
xSpan[1] = xSpan[0];
xSpan[0] = temp;
}
// Clip to the graphic area
if (y >= 0 && y < static_cast<int32>(height)) {
for (int i = 0; i < 2; i++) {
int32 &xVal = xSpan[i];
if (xVal < 0)
xVal = 0;
if (xVal >= static_cast<int32>(width))
xVal = width - 1;
}
int32 spanWidth = xSpan[1] - xSpan[0];
uint8 *bits = static_cast<uint8 *>(_mask->getBasePtr(xSpan[0], y));
for (int32 i = 0; i < spanWidth; i++)
bits[i] ^= 0xff;
}
}
}
}
} else if (shape == VisualElementRenderProperties::kShapeObsidianCanvasPuzzleTri1) {
// Upper-left right angle tri
_mask->clear(0);
for (int32 y = 0; y < height; y++) {
uint8 *scanline = static_cast<uint8 *>(_mask->getBasePtr(0, y));
int32 lineStart = 0;
int32 lineEnd = 64 - y;
for (int32 x = lineStart; x < lineEnd; x++)
scanline[x] = 0xff;
}
} else if (shape == VisualElementRenderProperties::kShapeObsidianCanvasPuzzleTri2) {
// Lower-left right-angle tri
_mask->clear(0);
for (int32 y = 0; y < height; y++) {
uint8 *scanline = static_cast<uint8 *>(_mask->getBasePtr(0, y));
int32 lineStart = 0;
int32 lineEnd = y;
for (int32 x = lineStart; x < lineEnd; x++)
scanline[x] = 0xff;
}
} else if (shape == VisualElementRenderProperties::kShapeObsidianCanvasPuzzleTri3) {
// Upper-right right-angle tri
_mask->clear(0);
for (int32 y = 0; y < height; y++) {
uint8 *scanline = static_cast<uint8 *>(_mask->getBasePtr(0, y));
int32 lineStart = y;
int32 lineEnd = 64;
for (int32 x = lineStart; x < lineEnd; x++)
scanline[x] = 0xff;
}
} else if (shape == VisualElementRenderProperties::kShapeObsidianCanvasPuzzleTri4) {
// Lower-right right-angle tri
_mask->clear(0);
for (int32 y = 0; y < height; y++) {
uint8 *scanline = static_cast<uint8 *>(_mask->getBasePtr(0, y));
int32 lineStart = 64 - y;
int32 lineEnd = 64;
for (int32 x = lineStart; x < lineEnd; x++)
scanline[x] = 0xff;
}
} else if (shape != VisualElementRenderProperties::kShapeRect) {
warning("Unimplemented graphic shape");
return;
}
}
Common::Rect srcRect = Common::Rect(0, 0, _rect.width(), _rect.height());
Common::Rect drawRect = srcRect;
drawRect.translate(_cachedAbsoluteOrigin.x, _cachedAbsoluteOrigin.y);
Common::Rect windowRect = Common::Rect(0, 0, window->getWidth(), window->getHeight());
Common::Rect clippedDrawRect = drawRect.findIntersectingRect(windowRect);
Common::Rect clippedSrcRect = srcRect;
clippedSrcRect.left += clippedDrawRect.left - drawRect.left;
clippedSrcRect.top += clippedDrawRect.top - drawRect.top;
clippedSrcRect.right += clippedDrawRect.right - drawRect.right;
clippedSrcRect.bottom += clippedDrawRect.bottom - drawRect.bottom;
if (clippedSrcRect.isEmpty())
return;
int32 srcToDestY = clippedDrawRect.top - clippedSrcRect.top;
switch (_renderProps.getInkMode()) {
case VisualElementRenderProperties::kInkModeBackgroundTransparent:
case VisualElementRenderProperties::kInkModeBackgroundMatte:
// Background transparent and background matte seem to have no effect on simple graphics,
// even if the foreground and background color are the same
case VisualElementRenderProperties::kInkModeCopy: {
const Graphics::PixelFormat &pixFmt = window->getPixelFormat();
const ColorRGB8 fillColorRGB8 = _renderProps.getForeColor();
uint32 fillColor = pixFmt.ARGBToColor(255, fillColorRGB8.r, fillColorRGB8.g, fillColorRGB8.b);
for (int32 srcY = clippedSrcRect.top; srcY < clippedSrcRect.bottom; srcY++) {
int32 spanWidth = clippedDrawRect.width();
void *destPixels = window->getSurface()->getBasePtr(clippedDrawRect.left, srcY + srcToDestY);
if (_mask) {
const uint8 *maskBytes = static_cast<const uint8 *>(_mask->getBasePtr(clippedSrcRect.left, srcY));
if (pixFmt.bytesPerPixel == 1) {
for (int32 x = 0; x < spanWidth; x++) {
if (maskBytes[x])
static_cast<uint8 *>(destPixels)[x] = fillColor;
}
} else if (pixFmt.bytesPerPixel == 2) {
for (int32 x = 0; x < spanWidth; x++) {
if (maskBytes[x])
static_cast<uint16 *>(destPixels)[x] = fillColor;
}
} else if (pixFmt.bytesPerPixel == 4) {
for (int32 x = 0; x < spanWidth; x++) {
if (maskBytes[x])
static_cast<uint32 *>(destPixels)[x] = fillColor;
}
}
} else {
if (pixFmt.bytesPerPixel == 1) {
for (int32 x = 0; x < spanWidth; x++)
static_cast<uint8 *>(destPixels)[x] = fillColor;
} else if (pixFmt.bytesPerPixel == 2) {
for (int32 x = 0; x < spanWidth; x++)
static_cast<uint16 *>(destPixels)[x] = fillColor;
} else if (pixFmt.bytesPerPixel == 4) {
for (int32 x = 0; x < spanWidth; x++)
static_cast<uint32 *>(destPixels)[x] = fillColor;
}
}
}
} break;
case VisualElementRenderProperties::kInkModeXor: {
const Graphics::PixelFormat &pixFmt = window->getPixelFormat();
uint32 colorMask = 0xff;
if (pixFmt.bytesPerPixel > 1)
colorMask = pixFmt.ARGBToColor(0, 255, 255, 255);
for (int32 srcY = clippedSrcRect.top; srcY < clippedSrcRect.bottom; srcY++) {
int32 spanWidth = clippedDrawRect.width();
void *destPixels = window->getSurface()->getBasePtr(clippedDrawRect.left, srcY + srcToDestY);
if (_mask) {
const uint8 *maskBytes = static_cast<const uint8 *>(_mask->getBasePtr(clippedSrcRect.left, srcY));
if (pixFmt.bytesPerPixel == 1) {
for (int32 x = 0; x < spanWidth; x++) {
if (maskBytes[x])
static_cast<uint8 *>(destPixels)[x] ^= 0xff;
}
} else if (pixFmt.bytesPerPixel == 2) {
for (int32 x = 0; x < spanWidth; x++) {
if (maskBytes[x])
static_cast<uint16 *>(destPixels)[x] ^= colorMask;
}
} else if (pixFmt.bytesPerPixel == 4) {
for (int32 x = 0; x < spanWidth; x++) {
if (maskBytes[x])
static_cast<uint32 *>(destPixels)[x] ^= colorMask;
}
}
} else {
if (pixFmt.bytesPerPixel == 1) {
for (int32 x = 0; x < spanWidth; x++)
static_cast<uint8 *>(destPixels)[x] ^= 0xff;
} else if (pixFmt.bytesPerPixel == 2) {
for (int32 x = 0; x < spanWidth; x++)
static_cast<uint16 *>(destPixels)[x] ^= colorMask;
} else if (pixFmt.bytesPerPixel == 4) {
for (int32 x = 0; x < spanWidth; x++)
static_cast<uint32 *>(destPixels)[x] ^= colorMask;
}
}
}
} break;
case VisualElementRenderProperties::kInkModeInvisible:
case VisualElementRenderProperties::kInkModeDefault:
break;
default:
warning("Unimplemented graphic ink mode");
return;
}
// TODO: The accurate behavior for polys is complicated.
// It looks like the way that it works is that a "line mask" is constructed by inverting the mask, dilating it
// by HALF the bevel size, then masking that out using the original mask, which results in a border mask.
// Then, the bevel diagonal is computed as simply a line going from the lower-left corner to the top-right corner.
if (_interiorShading) {
const Graphics::PixelFormat &pixFmt = window->getPixelFormat();
if (pixFmt.bytesPerPixel > 1) {
uint32 rMask = pixFmt.ARGBToColor(0, 255, 0, 0);
uint32 gMask = pixFmt.ARGBToColor(0, 0, 255, 0);
uint32 bMask = pixFmt.ARGBToColor(0, 0, 0, 255);
uint32 rAdd = quantizeShading(rMask, _interiorShading);
uint32 gAdd = quantizeShading(gMask, _interiorShading);
uint32 bAdd = quantizeShading(bMask, _interiorShading);
bool isBrighten = (_interiorShading > 0);
Graphics::ManagedSurface *windowSurface = window->getSurface().get();
for (int32 srcY = clippedSrcRect.top; srcY < clippedSrcRect.bottom; srcY++) {
int32 spanWidth = clippedDrawRect.width();
int32 effectLength = 0;
if (_mask) {
const uint8 *maskBytes = static_cast<const uint8 *>(_mask->getBasePtr(clippedSrcRect.left, srcY));
for (int32 x = 0; x < spanWidth; x++) {
if (maskBytes[x])
effectLength++;
else {
if (effectLength > 0) {
void *effectPixels = windowSurface->getBasePtr(clippedDrawRect.left + x - effectLength, srcY + srcToDestY);
renderShadingScanlineDynamic(effectPixels, effectLength, rMask, rAdd, gMask, gAdd, bMask, bAdd, isBrighten, windowSurface->format.bytesPerPixel);
}
effectLength = 0;
}
}
} else
effectLength = spanWidth;
if (effectLength > 0) {
void *effectPixels = windowSurface->getBasePtr(clippedDrawRect.left + spanWidth - effectLength, srcY + srcToDestY);
renderShadingScanlineDynamic(effectPixels, effectLength, rMask, rAdd, gMask, gAdd, bMask, bAdd, isBrighten, windowSurface->format.bytesPerPixel);
}
}
}
}
}
MovieResizeFilter::~MovieResizeFilter() {
}
MovieElement::MovieElement()
: _cacheBitmap(false), _alternate(false), _playEveryFrame(false), _reversed(false), /* _haveFiredAtLastCel(false), */
/* _haveFiredAtFirstCel(false), */_shouldPlayIfNotPaused(true), _needsReset(true), _currentPlayState(kMediaStateStopped),
_assetID(0), _maxTimestamp(0), _timeScale(0), _currentTimestamp(0), _volume(100),
_displayFrame(nullptr) {
}
MovieElement::~MovieElement() {
if (_unloadSignaller)
_unloadSignaller->removeReceiver(this);
if (_playMediaSignaller)
_playMediaSignaller->removeReceiver(this);
stopSubtitles();
}
bool MovieElement::load(ElementLoaderContext &context, const Data::MovieElement &data) {
if (!loadCommon(data.name, data.guid, data.rect1, data.elementFlags, data.layer, data.streamLocator, data.sectionID))
return false;
_cacheBitmap = ((data.elementFlags & Data::ElementFlags::kCacheBitmap) != 0);
_paused = ((data.elementFlags & Data::ElementFlags::kPaused) != 0);
_loop = ((data.animationFlags & Data::AnimationFlags::kLoop) != 0);
_alternate = ((data.animationFlags & Data::AnimationFlags::kAlternate) != 0);
_playEveryFrame = ((data.animationFlags & Data::AnimationFlags::kPlayEveryFrame) != 0);
_assetID = data.assetID;
_volume = data.volume;
return true;
}
bool MovieElement::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
if (attrib == "range") {
result.setIntRange(_playRange);
return true;
}
if (attrib == "timevalue") {
result.setInt(_currentTimestamp);
return true;
}
if (attrib == "timescale") {
result.setInt(_timeScale);
return true;
}
return VisualElement::readAttribute(thread, result, attrib);
}
MiniscriptInstructionOutcome MovieElement::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) {
if (attrib == "range") {
DynamicValueWriteOrRefAttribFuncHelper<MovieElement, &MovieElement::scriptSetRange, &MovieElement::scriptRangeWriteRefAttribute>::create(this, result);
return kMiniscriptInstructionOutcomeContinue;
}
if (attrib == "volume") {
DynamicValueWriteFuncHelper<MovieElement, &MovieElement::scriptSetVolume, true>::create(this, result);
return kMiniscriptInstructionOutcomeContinue;
}
if (attrib == "timevalue") {
DynamicValueWriteFuncHelper<MovieElement, &MovieElement::scriptSetTimestamp, true>::create(this, result);
return kMiniscriptInstructionOutcomeContinue;
}
return VisualElement::writeRefAttribute(thread, result, attrib);
}
VThreadState MovieElement::consumeCommand(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
// The reaction to the Play command should be to fire Unpaused and then fire Played.
// At First Cel is NOT fired by Play commands for some reason.
if (Event(EventIDs::kPlay, 0).respondsTo(msg->getEvent())) {
if (_paused)
{
_paused = false;
Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event(EventIDs::kUnpause, 0), DynamicValue(), getSelfReference()));
Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
runtime->sendMessageOnVThread(dispatch);
}
{
Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event(EventIDs::kPlay, 0), DynamicValue(), getSelfReference()));
Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
runtime->sendMessageOnVThread(dispatch);
}
StartPlayingTaskData *startPlayingTaskData = runtime->getVThread().pushTask("MovieElement::startPlayingTask", this, &MovieElement::startPlayingTask);
startPlayingTaskData->runtime = runtime;
ChangeFlagTaskData *becomeVisibleTaskData = runtime->getVThread().pushTask("MovieElement::changeVisibilityTask", static_cast<VisualElement *>(this), &MovieElement::changeVisibilityTask);
becomeVisibleTaskData->desiredFlag = true;
becomeVisibleTaskData->runtime = runtime;
return kVThreadReturn;
}
if (Event(EventIDs::kStop, 0).respondsTo(msg->getEvent())) {
if (!_paused) {
stopSubtitles();
_paused = true;
Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event(EventIDs::kPause, 0), DynamicValue(), getSelfReference()));
Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
runtime->sendMessageOnVThread(dispatch);
}
{
Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event(EventIDs::kStop, 0), DynamicValue(), getSelfReference()));
Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
runtime->sendMessageOnVThread(dispatch);
}
ChangeFlagTaskData *becomeVisibleTaskData = runtime->getVThread().pushTask("MovieElement::changeVisibilityTask", static_cast<VisualElement *>(this), &MovieElement::changeVisibilityTask);
becomeVisibleTaskData->desiredFlag = false;
becomeVisibleTaskData->runtime = runtime;
return kVThreadReturn;
}
return VisualElement::consumeCommand(runtime, msg);
}
void MovieElement::activate() {
Project *project = getRuntime()->getProject();
Common::SharedPtr<Asset> asset = project->getAssetByID(_assetID).lock();
if (!asset) {
warning("Movie element references asset %i but the asset isn't loaded!", _assetID);
return;
}
if (asset->getAssetType() == kAssetTypeMovie) {
MovieAsset *movieAsset = static_cast<MovieAsset *>(asset.get());
size_t streamIndex = movieAsset->getStreamIndex();
int segmentIndex = project->getSegmentForStreamIndex(streamIndex);
project->openSegmentStream(segmentIndex);
Common::SeekableReadStream *stream = project->getStreamForSegment(segmentIndex);
if (!stream) {
warning("Movie element stream could not be opened");
return;
}
Video::QuickTimeDecoder *qtDecoder = new Video::QuickTimeDecoder();
qtDecoder->setVolume(_volume * 255 / 100);
_videoDecoder.reset(qtDecoder);
_damagedFrames = movieAsset->getDamagedFrames();
Common::SafeSeekableSubReadStream *movieDataStream;
if (movieAsset->getMovieDataSize() > 0) {
qtDecoder->setChunkBeginOffset(movieAsset->getMovieDataPos());
movieDataStream = new Common::SafeSeekableSubReadStream(stream, movieAsset->getMovieDataPos(), movieAsset->getMovieDataPos() + movieAsset->getMovieDataSize(), DisposeAfterUse::NO);
} else {
// If no data size, the movie data is all over the file and the MOOV atom may be after it.
movieDataStream = new Common::SafeSeekableSubReadStream(stream, 0, stream->size(), DisposeAfterUse::NO);
movieDataStream->seek(movieAsset->getMoovAtomPos());
}
if (!_videoDecoder->loadStream(movieDataStream))
_videoDecoder.reset();
else {
if (getRuntime()->getHacks().removeQuickTimeEdits)
qtDecoder->flattenEditLists();
_timeScale = qtDecoder->getTimeScale();
_maxTimestamp = qtDecoder->getDuration().convertToFramerate(qtDecoder->getTimeScale()).totalNumberOfFrames();
}
_unloadSignaller = project->notifyOnSegmentUnload(segmentIndex, this);
} else if (asset->getAssetType() == kAssetTypeAVIMovie) {
AVIMovieAsset *aviAsset = static_cast<AVIMovieAsset *>(asset.get());
Common::File *f = new Common::File();
if (!f->open(Common::Path(Common::String("VIDEO/") + aviAsset->getExtFileName()))) {
warning("Movie asset could not be opened");
delete f;
return;
}
Video::AVIDecoder *aviDec = new Video::AVIDecoder();
aviDec->setVolume(_volume * 255 / 100);
_videoDecoder.reset(aviDec);
if (!_videoDecoder->loadStream(f))
_videoDecoder.reset();
else {
_timeScale = 1000;
_maxTimestamp = aviDec->getDuration().convertToFramerate(1000).totalNumberOfFrames();
}
} else {
warning("Movie element referenced a non-movie asset");
return;
}
_playMediaSignaller = project->notifyOnPlayMedia(this);
_playRange = IntRange(0, 0);
_currentTimestamp = 0;
if (_name.empty())
_name = project->getAssetNameByID(_assetID);
const SubtitleTables &subtitleTables = project->getSubtitles();
if (subtitleTables.assetMapping) {
const Common::String *subSetIDPtr = subtitleTables.assetMapping->findSubtitleSetForAssetID(_assetID);
if (!subSetIDPtr) {
Common::String assetName = project->getAssetNameByID(_assetID);
subSetIDPtr = subtitleTables.assetMapping->findSubtitleSetForAssetName(assetName);
}
if (subSetIDPtr)
_subtitles.reset(new SubtitlePlayer(getRuntime(), *subSetIDPtr, subtitleTables));
}
}
void MovieElement::deactivate() {
if (_unloadSignaller) {
_unloadSignaller->removeReceiver(this);
_unloadSignaller.reset();
}
if (_playMediaSignaller) {
_playMediaSignaller->removeReceiver(this);
_playMediaSignaller.reset();
}
_videoDecoder.reset();
}
bool MovieElement::canAutoPlay() const {
return _visible && !_paused;
}
void MovieElement::queueAutoPlayEvents(Runtime *runtime, bool isAutoPlaying) {
// At First Cel event fires even if the movie isn't playing, and it fires before Played
if (_visible) {
Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event(EventIDs::kAtFirstCel, 0), DynamicValue(), getSelfReference()));
Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
runtime->queueMessage(dispatch);
}
VisualElement::queueAutoPlayEvents(runtime, isAutoPlaying);
}
void MovieElement::render(Window *window) {
const IntRange realRange = computeRealRange();
if (!_videoDecoder)
return;
if (_needsReset) {
_videoDecoder->setReverse(_reversed);
_videoDecoder->seek(Audio::Timestamp(0, _timeScale).addFrames(_currentTimestamp));
_videoDecoder->setEndTime(Audio::Timestamp(0, _timeScale).addFrames(_reversed ? realRange.min : realRange.max));
const Graphics::Surface *decodedFrame = _videoDecoder->decodeNextFrame();
if (decodedFrame) {
_displayFrame = decodedFrame;
_scaledFrame.reset();
}
_needsReset = false;
}
if (_displayFrame) {
const Graphics::Surface *displaySurface = _displayFrame;
if (_resizeFilter) {
if (!_scaledFrame)
_scaledFrame = _resizeFilter->scaleFrame(*_displayFrame, _currentTimestamp);
displaySurface = _scaledFrame->surfacePtr();
}
Graphics::ManagedSurface *target = window->getSurface().get();
Common::Rect srcRect(0, 0, displaySurface->w, displaySurface->h);
Common::Rect destRect(_cachedAbsoluteOrigin.x, _cachedAbsoluteOrigin.y, _cachedAbsoluteOrigin.x + _rect.width(), _cachedAbsoluteOrigin.y + _rect.height());
target->blitFrom(*displaySurface, srcRect, destRect);
}
}
void MovieElement::playMedia(Runtime *runtime, Project *project) {
// If this isn't visible, then it wasn't rendered
if (!_visible)
return;
if (_videoDecoder) {
bool checkContinuously = false;
if (_shouldPlayIfNotPaused) {
if (_paused) {
// Goal state is paused
if (_videoDecoder->isPlaying() && !_videoDecoder->isPaused()) {
_videoDecoder->pauseVideo(true);
_currentPlayState = kMediaStatePaused;
}
} else {
// Goal state is playing
if (!_videoDecoder->isPlaying())
_videoDecoder->start();
if (_videoDecoder->isPaused())
_videoDecoder->pauseVideo(false);
if (_currentPlayState != kMediaStatePlayingLastFrame)
_currentPlayState = kMediaStatePlaying;
checkContinuously = true;
}
} else {
// Goal state is stopped
if (_videoDecoder->isPlaying())
_videoDecoder->stop();
_currentPlayState = kMediaStateStopped;
}
const IntRange realRange = computeRealRange();
uint32 minTS = realRange.min;
uint32 maxTS = realRange.max;
uint32 targetTS = _currentTimestamp;
int framesDecodedThisFrame = 0;
if (_currentPlayState == kMediaStatePlaying) {
while (_videoDecoder->needsUpdate()) {
if (_playEveryFrame && framesDecodedThisFrame > 0)
break;
if (_damagedFrames.size()) {
bool frameIsDamaged = false;
int thisFrameNumber = _videoDecoder->getCurFrame() + framesDecodedThisFrame + 1;
for (int damagedFrame : _damagedFrames) {
if (static_cast<int>(damagedFrame) == thisFrameNumber) {
frameIsDamaged = true;
break;
}
}
if (frameIsDamaged)
_videoDecoder->seekToFrame(thisFrameNumber + 1);
}
const Graphics::Surface *decodedFrame = _videoDecoder->decodeNextFrame();
// QuickTimeDecoder doesn't return true for endOfVideo or false for needsUpdate until it
// tries decoding past the end, so we're assuming that the decoded frame memory stays valid until we
// actually have a new frame and continuing to use it.
if (decodedFrame) {
_contentsDirty = true;
framesDecodedThisFrame++;
_displayFrame = decodedFrame;
_scaledFrame.reset();
if (_playEveryFrame)
break;
}
}
if (_videoDecoder->endOfVideo())
targetTS = _reversed ? realRange.min : realRange.max;
else
targetTS = (_videoDecoder->getTime() * _timeScale + 500) / 1000;
}
if (framesDecodedThisFrame > 1)
debug(1, "Perf warning: %i video frames decoded in one frame", framesDecodedThisFrame);
if (targetTS < minTS)
targetTS = minTS;
if (targetTS > maxTS)
targetTS = maxTS;
// Sync TS to the end of video if we hit the end
bool triggerEndEvents = false;
if (_currentPlayState == kMediaStatePlayingLastFrame)
triggerEndEvents = true;
if (targetTS != _currentTimestamp) {
assert(!_paused);
// Check media cues
for (MediaCueState *mediaCue : _mediaCues)
mediaCue->checkTimestampChange(runtime, _currentTimestamp, targetTS, checkContinuously, true);
if (_subtitles)
_subtitles->update(_currentTimestamp * 1000 / _timeScale, targetTS * 1000 / _timeScale);
_currentTimestamp = targetTS;
if (_currentTimestamp == maxTS) {
if (maxTS == _maxTimestamp) {
// If this play range plays through to the end, then delay end events 1 frame so it has a chance to render
_currentPlayState = kMediaStatePlayingLastFrame;
} else
triggerEndEvents = true;
}
}
if (triggerEndEvents) {
if (!_loop) {
_paused = true;
stopSubtitles();
Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event(EventIDs::kPause, 0), DynamicValue(), getSelfReference()));
Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
runtime->queueMessage(dispatch);
_currentPlayState = kMediaStateStopped;
}
Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event(EventIDs::kAtLastCel, 0), DynamicValue(), getSelfReference()));
Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
runtime->queueMessage(dispatch);
// For some reason, At First Cel isn't fired for movies, even when they loop or are set to timevalue 0
_videoDecoder->stop();
_currentPlayState = kMediaStateStopped;
if (_loop) {
stopSubtitles();
_needsReset = true;
_currentTimestamp = _reversed ? realRange.max : realRange.min;
_contentsDirty = true;
}
}
}
}
void MovieElement::setResizeFilter(const Common::SharedPtr<MovieResizeFilter> &filter) {
_resizeFilter = filter;
}
#ifdef MTROPOLIS_DEBUG_ENABLE
void MovieElement::debugSkipMovies() {
if (_videoDecoder && !_videoDecoder->endOfVideo()) {
const IntRange realRange = computeRealRange();
_videoDecoder->seek(Audio::Timestamp(0, _timeScale).addFrames(_reversed ? realRange.min : realRange.max));
}
}
#endif
void MovieElement::onSegmentUnloaded(int segmentIndex) {
_videoDecoder.reset();
}
IntRange MovieElement::computeRealRange() const {
// The default range for movies is 0..0, which is interpreted as unset
if (_playRange.min == 0 && _playRange.max == 0)
return IntRange(0, _maxTimestamp);
return _playRange;
}
void MovieElement::stopSubtitles() {
if (_subtitles)
_subtitles->stop();
}
void MovieElement::onPauseStateChanged() {
VisualElement::onPauseStateChanged();
if (_paused && _subtitles)
_subtitles->stop();
}
MiniscriptInstructionOutcome MovieElement::scriptSetRange(MiniscriptThread *thread, const DynamicValue &value) {
if (value.getType() != DynamicValueTypes::kIntegerRange) {
thread->error("Wrong type for movie element range");
return kMiniscriptInstructionOutcomeFailed;
}
return scriptSetRangeTyped(thread, value.getIntRange());
}
MiniscriptInstructionOutcome MovieElement::scriptSetVolume(MiniscriptThread *thread, const DynamicValue &value) {
int32 asInteger = 0;
if (!value.roundToInt(asInteger)) {
thread->error("Wrong type for movie element range");
return kMiniscriptInstructionOutcomeFailed;
}
if (asInteger < 0)
asInteger = 0;
else if (asInteger > 100)
asInteger = 100;
_volume = asInteger;
if (_videoDecoder)
_videoDecoder->setVolume(_volume * 255 / 100);
return kMiniscriptInstructionOutcomeContinue;
}
MiniscriptInstructionOutcome MovieElement::scriptSetTimestamp(MiniscriptThread *thread, const DynamicValue &value) {
int32 asInteger = 0;
if (!value.roundToInt(asInteger)) {
thread->error("Wrong type for movie element timevalue");
return kMiniscriptInstructionOutcomeFailed;
}
if (asInteger < _playRange.min)
asInteger = _playRange.min;
else if (asInteger > _playRange.max)
asInteger = _playRange.max;
if (asInteger != (int32)_currentTimestamp) {
SeekToTimeTaskData *taskData = thread->getRuntime()->getVThread().pushTask("MovieElement::seekToTimeTask", this, &MovieElement::seekToTimeTask);
taskData->runtime = getRuntime();
taskData->timestamp = asInteger;
return kMiniscriptInstructionOutcomeYieldToVThreadNoRetry;
}
return kMiniscriptInstructionOutcomeContinue;
}
MiniscriptInstructionOutcome MovieElement::scriptSetRangeStart(MiniscriptThread *thread, const DynamicValue &value) {
int32 asInteger = 0;
if (!value.roundToInt(asInteger)) {
thread->error("Couldn't set movie element range start");
return kMiniscriptInstructionOutcomeFailed;
}
int32 rangeMax = _playRange.max;
if (rangeMax < asInteger)
rangeMax = asInteger;
return scriptSetRangeTyped(thread, IntRange(asInteger, rangeMax));
}
MiniscriptInstructionOutcome MovieElement::scriptSetRangeEnd(MiniscriptThread *thread, const DynamicValue &value) {
int32 asInteger = 0;
if (!value.roundToInt(asInteger)) {
thread->error("Couldn't set movie element range end");
return kMiniscriptInstructionOutcomeFailed;
}
int32 rangeMin = _playRange.min;
if (rangeMin > asInteger)
rangeMin = asInteger;
return scriptSetRangeTyped(thread, IntRange(rangeMin, asInteger));
}
MiniscriptInstructionOutcome MovieElement::scriptRangeWriteRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) {
if (attrib == "start") {
DynamicValueWriteFuncHelper<MovieElement, &MovieElement::scriptSetRangeStart, true>::create(this, result);
return kMiniscriptInstructionOutcomeFailed;
}
if (attrib == "end") {
DynamicValueWriteFuncHelper<MovieElement, &MovieElement::scriptSetRangeStart, true>::create(this, result);
return kMiniscriptInstructionOutcomeFailed;
}
return kMiniscriptInstructionOutcomeFailed;
}
MiniscriptInstructionOutcome MovieElement::scriptSetRangeTyped(MiniscriptThread *thread, const IntRange &range) {
_playRange = range;
if (_playRange.min < 0)
_playRange.min = 0;
if (_playRange.max > (int32)_maxTimestamp)
_playRange.max = _maxTimestamp;
// Tested that this is the correct logic for inverted ranges
if (_playRange.max < _playRange.min)
_playRange.min = _playRange.max;
uint32 minTS = _playRange.min;
uint32 maxTS = _playRange.max;
uint32 targetTS = _currentTimestamp;
// If the current timestamp is out of range then it goes to the start of the range
// Obsidian needs this for the cube maze security booth
if (targetTS < minTS || targetTS > maxTS)
targetTS = _reversed ? maxTS : minTS;
if (targetTS != _currentTimestamp) {
SeekToTimeTaskData *taskData = thread->getRuntime()->getVThread().pushTask("MovieElement::seekToTimeTask", this, &MovieElement::seekToTimeTask);
taskData->runtime = getRuntime();
taskData->timestamp = targetTS;
return kMiniscriptInstructionOutcomeYieldToVThreadNoRetry;
}
return kMiniscriptInstructionOutcomeContinue;
}
VThreadState MovieElement::startPlayingTask(const StartPlayingTaskData &taskData) {
if (_videoDecoder) {
_videoDecoder->stop();
_currentPlayState = kMediaStateStopped;
_needsReset = true;
_contentsDirty = true;
_currentTimestamp = _reversed ? _playRange.max : _playRange.min;
_shouldPlayIfNotPaused = true;
_paused = false;
stopSubtitles();
}
return kVThreadReturn;
}
VThreadState MovieElement::seekToTimeTask(const SeekToTimeTaskData &taskData) {
uint32 minTS = _playRange.min;
uint32 maxTS = _playRange.max;
uint32 targetTS = taskData.timestamp;
if (targetTS < minTS)
targetTS = minTS;
if (targetTS > maxTS)
targetTS = maxTS;
if (targetTS == _currentTimestamp)
return kVThreadReturn;
_currentTimestamp = targetTS;
if (_videoDecoder) {
_videoDecoder->stop();
_currentPlayState = kMediaStateStopped;
}
_needsReset = true;
_contentsDirty = true;
stopSubtitles();
return kVThreadReturn;
}
ImageElement::ImageElement() : _cacheBitmap(false), _assetID(0) {
}
ImageElement::~ImageElement() {
}
bool ImageElement::load(ElementLoaderContext &context, const Data::ImageElement &data) {
if (!VisualElement::loadCommon(data.name, data.guid, data.rect1, data.elementFlags, data.layer, data.streamLocator, data.sectionID))
return false;
_cacheBitmap = ((data.elementFlags & Data::ElementFlags::kCacheBitmap) != 0);
_assetID = data.imageAssetID;
return true;
}
bool ImageElement::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
if (attrib == "text") {
// Obsidian accesses this on an image element in the menus, and if it fails, the "save first" warning
// prompt buttons aren't layered correctly?
result.setString(_text);
return true;
}
return VisualElement::readAttribute(thread, result, attrib);
}
MiniscriptInstructionOutcome ImageElement::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) {
if (attrib == "text") {
DynamicValueWriteStringHelper::create(&_text, writeProxy);
return kMiniscriptInstructionOutcomeContinue;
} else if (attrib == "flushpriority") {
DynamicValueWriteFuncHelper<ImageElement, &ImageElement::scriptSetFlushPriority, true>::create(this, writeProxy);
return kMiniscriptInstructionOutcomeContinue;
}
return VisualElement::writeRefAttribute(thread, writeProxy, attrib);
}
void ImageElement::activate() {
Project *project = getRuntime()->getProject();
Common::SharedPtr<Asset> asset = project->getAssetByID(_assetID).lock();
if (!asset) {
warning("Image element references asset %i but the asset isn't loaded!", _assetID);
return;
}
if (asset->getAssetType() != kAssetTypeImage) {
warning("Image element assigned an asset that isn't an image");
return;
}
_cachedImage = static_cast<ImageAsset *>(asset.get())->loadAndCacheImage(getRuntime());
if (_name.empty())
_name = project->getAssetNameByID(_assetID);
}
void ImageElement::deactivate() {
_cachedImage.reset();
}
void ImageElement::render(Window *window) {
if (_cachedImage) {
VisualElementRenderProperties::InkMode inkMode = _renderProps.getInkMode();
if (inkMode == VisualElementRenderProperties::kInkModeInvisible)
return;
Common::SharedPtr<Graphics::ManagedSurface> optimized = _cachedImage->optimize(getRuntime());
Common::Rect srcRect(optimized->w, optimized->h);
Common::Rect destRect(_cachedAbsoluteOrigin.x, _cachedAbsoluteOrigin.y, _cachedAbsoluteOrigin.x + _rect.width(), _cachedAbsoluteOrigin.y + _rect.height());
if (optimized->format.bytesPerPixel == 1) {
// FIXME: Pass palette to blit functions instead
if (_cachedImage->getOriginalColorDepth() == kColorDepthMode1Bit) {
const uint8 bwPalette[2 * 3] = {
255, 255, 255,
0, 0, 0
};
optimized->setPalette(bwPalette, 0, 2);
} else {
const Palette *palette = getPalette().get();
if (!palette)
palette = &getRuntime()->getGlobalPalette();
optimized->setPalette(palette->getPalette(), 0, 256);
}
}
uint8 alpha = _transitionProps.getAlpha();
Graphics::Surface *postShadingSource = optimized->surfacePtr();
Graphics::Surface tempSurface;
if (_interiorShading != 0 || (_bevelSize > 0 && (_bottomRightBevelShading != 0 || _topLeftBevelShading != 0))) {
tempSurface.copyFrom(*postShadingSource);
renderShading(tempSurface);
postShadingSource = &tempSurface;
}
if (inkMode == VisualElementRenderProperties::kInkModeBackgroundMatte || inkMode == VisualElementRenderProperties::kInkModeBackgroundTransparent) {
const ColorRGB8 transColorRGB8 = _renderProps.getBackColor();
uint32 transColor = optimized->format.ARGBToColor(0, transColorRGB8.r, transColorRGB8.g, transColorRGB8.b);
// Awful hack to work around transBlit not working with either 0 or -1
if (transColor == 0)
transColor = optimized->format.ARGBToColor(255, transColorRGB8.r, transColorRGB8.g, transColorRGB8.b);
window->getSurface()->transBlitFrom(*optimized, srcRect, destRect, transColor, false, 0, alpha);
} else if (inkMode == VisualElementRenderProperties::kInkModeDefault || inkMode == VisualElementRenderProperties::kInkModeCopy) {
if (alpha != 255) {
warning("Alpha fade was applied to a default or copy image, this isn't supported yet");
_transitionProps.setAlpha(255);
}
window->getSurface()->blitFrom(*optimized, srcRect, destRect);
} else {
warning("Unimplemented image ink mode");
}
}
}
#ifdef MTROPOLIS_DEBUG_ENABLE
void ImageElement::debugInspect(IDebugInspectionReport *report) const {
VisualElement::debugInspect(report);
if (report->declareStatic("assetID"))
report->declareStaticContents(Common::String::format("%i", static_cast<int>(_assetID)));
}
#endif
MiniscriptInstructionOutcome ImageElement::scriptSetFlushPriority(MiniscriptThread *thread, const DynamicValue &value) {
// We don't support flushing media, and this value isn't readable, so just discard it
return kMiniscriptInstructionOutcomeContinue;
}
MToonElement::MToonElement()
: _cacheBitmap(false), _maintainRate(false), _assetID(0), _rateTimes100000(0), _flushPriority(0), _celStartTimeMSec(0),
_isPlaying(false), _isStopped(false), _renderedFrame(0), _playRange(IntRange(1, 1)), _cel(1), _hasIssuedRenderWarning(false) {
}
MToonElement::~MToonElement() {
if (_playMediaSignaller)
_playMediaSignaller->removeReceiver(this);
}
bool MToonElement::load(ElementLoaderContext &context, const Data::MToonElement &data) {
if (!loadCommon(data.name, data.guid, data.rect1, data.elementFlags, data.layer, data.streamLocator, data.sectionID))
return false;
_cacheBitmap = ((data.elementFlags & Data::ElementFlags::kCacheBitmap) != 0);
_paused = ((data.elementFlags & Data::ElementFlags::kPaused) != 0);
_loop = ((data.animationFlags & Data::AnimationFlags::kLoop) != 0);
_maintainRate = ((data.elementFlags & Data::AnimationFlags::kPlayEveryFrame) == 0); // NOTE: Inverted intentionally
_assetID = data.assetID;
_rateTimes100000 = data.rateTimes100000;
return true;
}
bool MToonElement::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
if (attrib == "cel") {
result.setInt(_cel);
return true;
} else if (attrib == "flushpriority") {
result.setInt(_flushPriority);
return true;
} else if (attrib == "rate") {
result.setFloat(_rateTimes100000 / 100000.0);
return true;
} else if (attrib == "range") {
result.setIntRange(_playRange);
return true;
} else if (attrib == "celcount") {
if (_cachedMToon)
result.setInt(_cachedMToon->getMetadata()->frames.size());
else
result.setInt(0);
return true;
} else if (attrib == "regpoint") {
result.setPoint(_cachedMToon->getMetadata()->registrationPoint);
return true;
}
return VisualElement::readAttribute(thread, result, attrib);
}
MiniscriptInstructionOutcome MToonElement::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) {
if (attrib == "cel") {
DynamicValueWriteFuncHelper<MToonElement, &MToonElement::scriptSetCel, true>::create(this, result);
return kMiniscriptInstructionOutcomeContinue;
} else if (attrib == "flushpriority") {
DynamicValueWriteIntegerHelper<int32>::create(&_flushPriority, result);
return kMiniscriptInstructionOutcomeContinue;
} else if (attrib == "maintainrate") {
DynamicValueWriteBoolHelper::create(&_maintainRate, result);
return kMiniscriptInstructionOutcomeContinue;
} else if (attrib == "rate") {
DynamicValueWriteFuncHelper<MToonElement, &MToonElement::scriptSetRate, true>::create(this, result);
return kMiniscriptInstructionOutcomeContinue;
} else if (attrib == "range") {
DynamicValueWriteOrRefAttribFuncHelper<MToonElement, &MToonElement::scriptSetRange, &MToonElement::scriptRangeWriteRefAttribute>::create(this, result);
return kMiniscriptInstructionOutcomeContinue;
}
return VisualElement::writeRefAttribute(thread, result, attrib);
}
VThreadState MToonElement::consumeCommand(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
if (Event(EventIDs::kPlay, 0).respondsTo(msg->getEvent())) {
// If the range set fails, then the mToon should play anyway, so ignore the result
(void)scriptSetRange(nullptr, msg->getValue());
StartPlayingTaskData *startPlayingTaskData = runtime->getVThread().pushTask("MToonElement::startPlayingTask", this, &MToonElement::startPlayingTask);
startPlayingTaskData->runtime = runtime;
ChangeFlagTaskData *becomeVisibleTaskData = runtime->getVThread().pushTask("MToonElement::changeVisibilityTask", static_cast<VisualElement *>(this), &MToonElement::changeVisibilityTask);
becomeVisibleTaskData->desiredFlag = true;
becomeVisibleTaskData->runtime = runtime;
if (_isStopped) {
_isStopped = false;
runtime->setSceneGraphDirty();
}
return kVThreadReturn;
}
if (Event(EventIDs::kStop, 0).respondsTo(msg->getEvent())) {
// mTropolis 1.0 will not fire a Hidden event when an mToon is stopped even though it is hidden in the process.
// MTI depends on this, otherwise 2 hints will play at once when clicking a song button on the piano.
// This same bug does NOT apply to the "Shown" event firing on Play (as happens above).
if (runtime->getProject()->guessVersion() >= MTropolisVersions::kMTropolisVersion1_1) {
ChangeFlagTaskData *hideTaskData = runtime->getVThread().pushTask("MToonElement::changeVisibilityTask", static_cast<VisualElement *>(this), &MToonElement::changeVisibilityTask);
hideTaskData->desiredFlag = false;
hideTaskData->runtime = runtime;
} else
setVisible(runtime, false);
StopPlayingTaskData *stopPlayingTaskData = runtime->getVThread().pushTask("MToonElement::stopPlayingTask", this, &MToonElement::stopPlayingTask);
stopPlayingTaskData->runtime = runtime;
return kVThreadReturn;
}
return VisualElement::consumeCommand(runtime, msg);
}
void MToonElement::activate() {
Project *project = getRuntime()->getProject();
Common::SharedPtr<Asset> asset = project->getAssetByID(_assetID).lock();
if (!asset) {
warning("mToon element references asset %i but the asset isn't loaded!", _assetID);
return;
}
if (asset->getAssetType() != kAssetTypeMToon) {
warning("mToon element assigned an asset that isn't an mToon");
return;
}
uint hackFlags = 0;
_cachedMToon = static_cast<MToonAsset *>(asset.get())->loadAndCacheMToon(getRuntime(), hackFlags);
_metadata = _cachedMToon->getMetadata();
_playMediaSignaller = project->notifyOnPlayMedia(this);
_playRange = IntRange(1, _metadata->frames.size());
if (_name.empty())
_name = project->getAssetNameByID(_assetID);
if (_hooks)
_hooks->onPostActivate(this);
}
void MToonElement::deactivate() {
if (_playMediaSignaller) {
_playMediaSignaller->removeReceiver(this);
_playMediaSignaller.reset();
}
_renderSurface.reset();
}
bool MToonElement::canAutoPlay() const {
return _visible && !_paused;
}
void MToonElement::render(Window *window) {
// Stopped mToons are not supposed to render
// FIXME: Should this also disable mouse collision? Should we detect ths somewhere else?
if (_isStopped)
return;
if (_cachedMToon) {
_cachedMToon->optimize(getRuntime());
uint32 frame = _cel - 1;
assert(frame < _metadata->frames.size());
_cachedMToon->getOrRenderFrame(_renderedFrame, frame, _renderSurface);
const Palette *palette = nullptr;
if (_renderSurface->format.bytesPerPixel == 1) {
palette = getPalette().get();
if (!palette)
palette = &getRuntime()->getGlobalPalette();
// FIXME: Should support passing the palette to the blit function instead
_renderSurface->setPalette(palette->getPalette(), 0, 256);
}
_renderedFrame = frame;
// This is a bit suboptimal since we don't need to render the frame if invisible, but
// we do need some things here to be up to date because isMouseCollisionAtPoint depends on
// invisible mToon frames still being clickable.
VisualElementRenderProperties::InkMode inkMode = _renderProps.getInkMode();
if (inkMode == VisualElementRenderProperties::kInkModeInvisible)
return;
if (_renderSurface) {
Common::Rect srcRect;
Common::Rect destRect;
Common::Rect frameRect = _metadata->frames[frame].rect;
if (frameRect.width() == _renderSurface->w && frameRect.height() == _renderSurface->h) {
// Frame rect is the size of the render surface, meaning the frame rect is an offset
srcRect = Common::Rect(0, 0, frameRect.width(), frameRect.height());
} else {
// Frame rect is a sub-area of the rendered rect
srcRect = Common::Rect(frameRect.left, frameRect.top, frameRect.right, frameRect.bottom);
}
destRect = Common::Rect(_cachedAbsoluteOrigin.x + frameRect.left, _cachedAbsoluteOrigin.y + frameRect.top, _cachedAbsoluteOrigin.x + frameRect.right, _cachedAbsoluteOrigin.y + frameRect.bottom);
if (inkMode == VisualElementRenderProperties::kInkModeBackgroundMatte || inkMode == VisualElementRenderProperties::kInkModeBackgroundTransparent) {
ColorRGB8 transColorRGB8 = _renderProps.getBackColor();
uint32 transColor = 0;
if (_renderSurface->format.bytesPerPixel == 1) {
assert(palette);
const byte *paletteData = palette->getPalette();
bool foundColor = false;
for (uint i = 0; i < Palette::kNumColors; i++) {
if (transColorRGB8 == ColorRGB8(paletteData[i * 3 + 0], paletteData[i * 3 + 1], paletteData[i * 3 + 2])) {
if (foundColor) {
warning("mToon is rendered color key but has multiple palette entries matching the transparent color, this may not render correctly");
_hasIssuedRenderWarning = true;
break;
} else {
foundColor = true;
transColor = i;
if (_hasIssuedRenderWarning)
break;
}
}
}
} else
transColor = _renderSurface->format.ARGBToColor(255, transColorRGB8.r, transColorRGB8.g, transColorRGB8.b);
window->getSurface()->transBlitFrom(*_renderSurface, srcRect, destRect, transColor);
} else if (inkMode == VisualElementRenderProperties::kInkModeCopy || inkMode == VisualElementRenderProperties::kInkModeDefault) {
window->getSurface()->blitFrom(*_renderSurface, srcRect, destRect);
} else {
warning("Unsupported mToon ink mode");
}
}
}
}
bool MToonElement::isMouseCollisionAtPoint(int32 relativeX, int32 relativeY) const {
relativeX -= _rect.left;
relativeY -= _rect.top;
if (_renderSurface) {
Common::Rect frameRect = _metadata->frames[_renderedFrame].rect;
if (relativeX < frameRect.left || relativeY < frameRect.top || relativeX >= frameRect.right || relativeY >= frameRect.bottom)
return false;
if (_renderProps.getInkMode() == VisualElementRenderProperties::kInkModeBackgroundMatte) {
// TODO: This doesn't account for scaling
ColorRGB8 transColorRGB8 = _renderProps.getBackColor();
uint32 transColor = _renderSurface->format.ARGBToColor(255, transColorRGB8.r, transColorRGB8.g, transColorRGB8.b);
if (frameRect.width() == _renderSurface->w && frameRect.height() == _renderSurface->h) {
// Frame rect is the size of the render surface, meaning the frame is floating and we need to adjust to its coordinates
relativeX -= frameRect.left;
relativeY -= frameRect.top;
}
// ... otherwise it's a sub-area of the rendered rect, meaning we shouldn't adjust coordinates
// Sanity-check
if (relativeX < 0 || relativeY < 0 || relativeX >= _renderSurface->w || relativeY >= _renderSurface->h)
return false;
// Check if the pixel is transparent
if (_renderSurface->getPixel(relativeX, relativeY) == transColor)
return false;
}
return true;
}
return false;
}
Common::Rect MToonElement::getRelativeCollisionRect() const {
Common::Rect colRect = _metadata->frames[_renderedFrame].rect;
colRect.translate(_rect.left, _rect.top);
return colRect;
}
#ifdef MTROPOLIS_DEBUG_ENABLE
void MToonElement::debugInspect(IDebugInspectionReport *report) const {
VisualElement::debugInspect(report);
report->declareDynamic("cel", Common::String::format("%i", static_cast<int>(_cel)));
report->declareDynamic("assetID", Common::String::format("%i", static_cast<int>(_assetID)));
report->declareDynamic("isPlaying", Common::String::format("%s", _isPlaying ? "true" : "false"));
report->declareDynamic("renderedFrame", Common::String::format("%i", static_cast<int>(_renderedFrame)));
report->declareDynamic("playRange", Common::String::format("%i-%i", static_cast<int>(_playRange.min), static_cast<int>(_playRange.max)));
}
#endif
VThreadState MToonElement::startPlayingTask(const StartPlayingTaskData &taskData) {
if (_rateTimes100000 < 0)
_cel = _playRange.max;
else
_cel = _playRange.min;
_paused = false;
_isPlaying = false; // Reset play state, it starts for real in playMedia
_contentsDirty = true;
// These send in reverse order
{
Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event(EventIDs::kAtFirstCel, 0), DynamicValue(), getSelfReference()));
Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
taskData.runtime->sendMessageOnVThread(dispatch);
}
{
Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event(EventIDs::kPlay, 0), DynamicValue(), getSelfReference()));
Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
taskData.runtime->sendMessageOnVThread(dispatch);
}
return kVThreadReturn;
}
VThreadState MToonElement::stopPlayingTask(const StopPlayingTaskData &taskData) {
_contentsDirty = true;
_isPlaying = false;
if (!_isStopped) {
_isStopped = true;
Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event(EventIDs::kStop, 0), DynamicValue(), getSelfReference()));
Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
taskData.runtime->sendMessageOnVThread(dispatch);
}
if (_hooks)
_hooks->onStopPlayingMToon(this, _visible, _isStopped, _renderSurface.get());
return kVThreadReturn;
}
void MToonElement::playMedia(Runtime *runtime, Project *project) {
if (_paused)
return;
// TODO: This is semi-accurate: mTropolis Player will advance mToon time while
// the element is hidden and can then fire a barrage of frame advances and events
// if it's revealed again. However, we're not fully handling that here yet,
// and we actually miss events if frame advance overruns the last cel, which can
// cause problems sometimes (e.g. lag in the Spider air puzzle in Obsidian when
// the board is revealed)
if (!_visible)
return;
int32 minCel = _playRange.min;
int32 maxCel = _playRange.max;
int32 sanitizeMaxCel = _metadata->frames.size();
int32 targetCel = _cel;
uint64 playTime = runtime->getPlayTime();
if (!_isPlaying) {
_isPlaying = true;
_celStartTimeMSec = runtime->getPlayTime();
}
const bool isReversed = (_rateTimes100000 < 0);
uint32 absRateTimes100000;
if (isReversed)
absRateTimes100000 = -_rateTimes100000;
else
absRateTimes100000 = _rateTimes100000;
// Might be possible due to drift?
if (playTime < _celStartTimeMSec)
return;
uint64 timeSinceCelStart = playTime - _celStartTimeMSec;
uint64 framesAdvanced = timeSinceCelStart * static_cast<uint64>(absRateTimes100000) / static_cast<uint64>(100000000);
if (framesAdvanced > 0) {
// This needs to be handled correctly: Reaching the last frame triggers At Last Cel or At First Cel,
// but going PAST the end frame triggers automatic stop and pause. The Obsidian bureau filing cabinets
// depend on this, since they reset the cel when reaching the last cel but do not unpause.
// There's actually some weird stuff we don't handle here where the play control range is invalid, in
// which case the timing of "at last cel"/"at first cel" triggers based on where the timer would be
// in the invalid range, so mTropolis Player apparently keeps a play cel independent of the actual
// cel?
bool ranPastEnd = false;
size_t framesRemainingToOnePastEnd = isReversed ? (_cel - minCel + 1) : (maxCel + 1 - _cel);
bool alreadyAtLastCel = (framesRemainingToOnePastEnd == 1);
if (framesRemainingToOnePastEnd <= framesAdvanced) {
ranPastEnd = true;
if (_loop)
targetCel = isReversed ? maxCel : minCel;
else
targetCel = isReversed ? minCel : maxCel;
} else
targetCel = isReversed ? (_cel - framesAdvanced) : (_cel + framesAdvanced);
if (targetCel < 1)
targetCel = 1;
if (targetCel > sanitizeMaxCel)
targetCel = sanitizeMaxCel;
if (_cel != targetCel) {
_cel = targetCel;
_contentsDirty = true;
}
// Events play control events even if no cel advance occurs
bool atFirstCel = (targetCel == (isReversed ? maxCel : minCel));
bool atLastCel = (targetCel == (isReversed ? minCel : maxCel)) && !(ranPastEnd && alreadyAtLastCel);
if (atFirstCel) {
Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event(EventIDs::kAtFirstCel, 0), DynamicValue(), getSelfReference()));
Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
runtime->queueMessage(dispatch);
} else if (atLastCel) { // These can not fire from the same frame transition (see notes)
Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event(EventIDs::kAtLastCel, 0), DynamicValue(), getSelfReference()));
Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
runtime->queueMessage(dispatch);
}
if (ranPastEnd && !_loop) {
_paused = true;
Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event(EventIDs::kPause, 0), DynamicValue(), getSelfReference()));
Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
runtime->queueMessage(dispatch);
}
if (_maintainRate && !runtime->getHacks().ignoreMToonMaintainRateFlag)
_celStartTimeMSec = playTime;
else
_celStartTimeMSec += (static_cast<uint64>(100000000) * framesAdvanced) / absRateTimes100000;
}
}
MiniscriptInstructionOutcome MToonElement::scriptSetCel(MiniscriptThread *thread, const DynamicValue &value) {
int32 newCel = 0;
if (!value.roundToInt(newCel)) {
thread->error("Attempted to set mToon cel to an invalid value");
return kMiniscriptInstructionOutcomeFailed;
}
int32 maxCel = _metadata->frames.size();
// Intentially ignore play range. The cel may be set to an out-of-range cel here and will
// in fact play from that cel even if it's out of range. The mariachi hint room near the
// Bureau booths in Obsidian depends on this behavior, since it sets the mToon cel and then
// sets the range based on the cel value.
//
// We also need to loop around to 1 (exactly) if the range is exceeded. MTI depends on this
// in the piano to loop the sound bank display mToon.
if (newCel < 1 || newCel > maxCel)
newCel = 1;
if (newCel != _cel) {
_cel = newCel;
_contentsDirty = true;
}
return kMiniscriptInstructionOutcomeContinue;
}
MiniscriptInstructionOutcome MToonElement::scriptSetRange(MiniscriptThread *thread, const DynamicValue &value) {
if (value.getType() == DynamicValueTypes::kIntegerRange)
return scriptSetRangeTyped(thread, value.getIntRange());
if (value.getType() == DynamicValueTypes::kPoint)
return scriptSetRangeTyped(thread, value.getPoint());
if (value.getType() == DynamicValueTypes::kLabel)
return scriptSetRangeTyped(thread, value.getLabel());
if (thread)
thread->error("Invalid type for mToon range");
return kMiniscriptInstructionOutcomeFailed;
}
MiniscriptInstructionOutcome MToonElement::scriptSetRangeStart(MiniscriptThread *thread, const DynamicValue &value) {
int32 asInteger = 0;
if (!value.roundToInt(asInteger)) {
thread->error("Invalid type for mToon range start");
return kMiniscriptInstructionOutcomeFailed;
}
IntRange range = _playRange;
range.min = asInteger;
return scriptSetRangeTyped(thread, range);
}
MiniscriptInstructionOutcome MToonElement::scriptSetRangeEnd(MiniscriptThread *thread, const DynamicValue &value) {
int32 asInteger = 0;
if (!value.roundToInt(asInteger)) {
thread->error("Invalid type for mToon range start");
return kMiniscriptInstructionOutcomeFailed;
}
IntRange range = _playRange;
range.max = asInteger;
return scriptSetRangeTyped(thread, range);
}
MiniscriptInstructionOutcome MToonElement::scriptRangeWriteRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) {
if (attrib == "start") {
DynamicValueWriteFuncHelper<MToonElement, &MToonElement::scriptSetRangeStart, true>::create(this, result);
return kMiniscriptInstructionOutcomeContinue;
} else if (attrib == "end") {
DynamicValueWriteFuncHelper<MToonElement, &MToonElement::scriptSetRangeEnd, true>::create(this, result);
return kMiniscriptInstructionOutcomeContinue;
}
return kMiniscriptInstructionOutcomeFailed;
}
MiniscriptInstructionOutcome MToonElement::scriptSetRangeTyped(MiniscriptThread *thread, const IntRange &intRangeRef) {
IntRange intRange = intRangeRef;
int32 maxFrame = _metadata->frames.size();
// Intentionally buggy sanitization, see notes.
const bool isInvertedRange = (intRange.min > intRange.max);
if (intRange.min < 1)
intRange.min = 1;
if (intRange.max > maxFrame)
intRange.max = maxFrame;
if (isInvertedRange) {
// coverity[swapped_arguments]
_playRange = IntRange(intRange.max, intRange.min);
if (_rateTimes100000 > 0)
_rateTimes100000 = -_rateTimes100000;
} else {
_playRange = intRange;
if (_rateTimes100000 < 0)
_rateTimes100000 = -_rateTimes100000;
}
int32 newCel = _cel;
if (newCel < intRange.min || newCel > intRange.max)
newCel = intRange.min;
if (newCel < 1 || newCel > maxFrame)
newCel = maxFrame;
if (newCel != _cel) {
_cel = newCel;
_contentsDirty = true;
}
return kMiniscriptInstructionOutcomeContinue;
}
MiniscriptInstructionOutcome MToonElement::scriptSetRangeTyped(MiniscriptThread *thread, const Common::Point &pointRef) {
IntRange intRange(pointRef.x, pointRef.y);
return scriptSetRangeTyped(thread, intRange);
}
MiniscriptInstructionOutcome MToonElement::scriptSetRangeTyped(MiniscriptThread *thread, const Label &label) {
const Common::String *nameStrPtr = getRuntime()->getProject()->findNameOfLabel(label);
if (!nameStrPtr) {
if (thread)
thread->error("mToon range label wasn't found");
return kMiniscriptInstructionOutcomeFailed;
}
if (!_metadata) {
if (thread)
thread->error("mToon range couldn't be resolved because the metadata wasn't loaded yet");
return kMiniscriptInstructionOutcomeFailed;
}
for (const MToonMetadata::FrameRangeDef &frameRange : _metadata->frameRanges) {
if (caseInsensitiveEqual(frameRange.name, *nameStrPtr)) {
// Frame ranges in the metadata are 0-based, but setting the range is 1-based, so add 1
return scriptSetRangeTyped(thread, IntRange(frameRange.startFrame + 1, frameRange.endFrame + 1));
}
}
if (thread)
thread->error("mToon range was assigned to a label but the label doesn't exist in the mToon data");
return kMiniscriptInstructionOutcomeFailed;
}
void MToonElement::onPauseStateChanged() {
_celStartTimeMSec = getRuntime()->getPlayTime();
}
MiniscriptInstructionOutcome MToonElement::scriptSetRate(MiniscriptThread *thread, const DynamicValue &value) {
switch (value.getType()) {
case DynamicValueTypes::kFloat:
_rateTimes100000 = static_cast<int32>(round(value.getFloat()) * 100000.0);
break;
case DynamicValueTypes::kInteger:
_rateTimes100000 = value.getInt() * 100000;
break;
default:
thread->error("Invalid type for Miniscript rate");
return kMiniscriptInstructionOutcomeFailed;
}
_celStartTimeMSec = thread->getRuntime()->getPlayTime();
return kMiniscriptInstructionOutcomeContinue;
}
TextLabelElement::TextLabelElement()
: _cacheBitmap(false), _needsRender(false), /*_isBitmap(false), */_assetID(0),
_macFontID(0), _size(12), _alignment(kTextAlignmentLeft) {
}
TextLabelElement::~TextLabelElement() {
}
bool TextLabelElement::isTextLabel() const {
return true;
}
bool TextLabelElement::load(ElementLoaderContext &context, const Data::TextLabelElement &data) {
if (!loadCommon(data.name, data.guid, data.rect1, data.elementFlags, data.layer, 0, data.sectionID))
return false;
_cacheBitmap = ((data.elementFlags & Data::ElementFlags::kCacheBitmap) != 0);
_assetID = data.assetID;
return true;
}
bool TextLabelElement::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
if (attrib == "text") {
result.setString(_text);
return true;
}
return VisualElement::readAttribute(thread, result, attrib);
}
bool TextLabelElement::readAttributeIndexed(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib, const DynamicValue &index) {
if (attrib == "line") {
int32 asInteger = 0;
if (!index.roundToInt(asInteger) || asInteger < 1) {
thread->error("Invalid text label line index");
return false;
}
size_t lineIndex = static_cast<size_t>(asInteger) - 1;
uint32 startPos;
uint32 endPos;
if (findLineRange(lineIndex, startPos, endPos))
result.setString(_text.substr(startPos, endPos - startPos));
else
result.setString("");
return true;
}
return VisualElement::readAttributeIndexed(thread, result, attrib, index);
}
MiniscriptInstructionOutcome TextLabelElement::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) {
if (attrib == "text") {
DynamicValueWriteFuncHelper<TextLabelElement, &TextLabelElement::scriptSetText, true>::create(this, writeProxy);
return kMiniscriptInstructionOutcomeContinue;
}
return VisualElement::writeRefAttribute(thread, writeProxy, attrib);
}
MiniscriptInstructionOutcome TextLabelElement::writeRefAttributeIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib, const DynamicValue &index) {
if (attrib == "line") {
int32 asInteger = 0;
if (!index.roundToInt(asInteger) || asInteger < 1) {
thread->error("Invalid text label line set index");
return kMiniscriptInstructionOutcomeFailed;
}
writeProxy.pod.ifc = DynamicValueWriteInterfaceGlue<TextLabelLineWriteInterface>::getInstance();
writeProxy.pod.objectRef = this;
writeProxy.pod.ptrOrOffset = static_cast<uintptr>(asInteger) - 1;
return kMiniscriptInstructionOutcomeContinue;
}
return VisualElement::writeRefAttributeIndexed(thread, writeProxy, attrib, index);
}
void TextLabelElement::activate() {
Project *project = getRuntime()->getProject();
Common::SharedPtr<Asset> asset = project->getAssetByID(_assetID).lock();
if (!asset) {
warning("Text element references asset %i but the asset isn't loaded!", _assetID);
return;
}
if (asset->getAssetType() != kAssetTypeText) {
warning("Text element assigned an asset that isn't text");
return;
}
TextAsset *textAsset = static_cast<TextAsset *>(asset.get());
if (textAsset->isBitmap()) {
_renderedText = textAsset->getBitmapSurface();
_needsRender = false;
} else {
_needsRender = true;
_text = textAsset->getString();
_macFormattingSpans = textAsset->getMacFormattingSpans();
}
}
void TextLabelElement::deactivate() {
}
void TextLabelElement::render(Window *window) {
if (!_visible)
return;
int renderWidth = _rect.width();
int renderHeight = _rect.height();
if (_renderedText) {
if (renderWidth != _renderedText->w || renderHeight != _renderedText->h)
_needsRender = true;
}
if (_needsRender) {
_needsRender = false;
_renderedText.reset();
_renderedText.reset(new Graphics::ManagedSurface());
_renderedText->create(renderWidth, renderHeight, Graphics::PixelFormat::createFormatCLUT8());
_renderedText->fillRect(Common::Rect(0, 0, renderWidth, renderHeight), 0);
const Graphics::Font *font = nullptr;
if (_fontFamilyName.size() > 0) {
font = FontMan.getFontByName(_fontFamilyName.c_str());
if (!font)
font = FontMan.getFontByUsage(getDefaultUsageForNamedFont(_fontFamilyName, _size));
} else if (_macFontID != 0) {
// TODO: Formatting spans
int slant = 0;
// FIXME/HACK: These aren't public...
if (_styleFlags.bold)
slant |= 1;
if (_styleFlags.italic)
slant |= 2;
if (_styleFlags.underline)
slant |= 4;
if (_styleFlags.outline)
slant |= 8;
if (_styleFlags.shadow)
slant |= 16;
if (_styleFlags.condensed)
slant |= 32;
if (_styleFlags.expanded)
slant |= 64;
const Graphics::FontManager::FontUsage defaultUsage = getDefaultUsageForMacFont(_macFontID, _size);
const Graphics::Font *fallback = FontMan.getFontByUsage(defaultUsage);
Graphics::MacFont macFont(_macFontID, _size, slant);
macFont.setFallback(fallback);
font = getRuntime()->getMacFontManager()->getFont(macFont);
}
// Some weird cases (like the Immediate Action entryway in Obsidian) have no font info at all
if (!font)
font = FontMan.getFontByUsage(Graphics::FontManager::kGUIFont);
int height = font->getFontHeight();
int ascent = font->getFontAscent();
Graphics::TextAlign textAlign = Graphics::kTextAlignLeft;
switch (_alignment) {
case kTextAlignmentLeft:
textAlign = Graphics::kTextAlignLeft;
break;
case kTextAlignmentCenter:
textAlign = Graphics::kTextAlignCenter;
break;
case kTextAlignmentRight:
textAlign = Graphics::kTextAlignRight;
break;
default:
break;
}
int line = 0;
uint32 lineStart = 0;
while (lineStart < _text.size()) {
bool noMoreLines = false;
uint32 lineEndPos = _text.find('\r', lineStart);
if (lineEndPos == Common::String::npos) {
lineEndPos = _text.size();
noMoreLines = true;
}
Common::String lineStr;
if (lineStart != 0 || lineEndPos != _text.size())
lineStr = _text.substr(lineStart, lineEndPos - lineStart);
else
lineStr = _text;
// Split the line into sublines
while (lineStr.size() > 0) {
size_t lineCommitted = 0;
bool prevWasWhitespace = true;
for (size_t i = 0; i <= lineStr.size(); i++) {
bool isWhitespace = (i == lineStr.size() || lineStr[i] < ' ');
if (isWhitespace) {
if (!prevWasWhitespace) {
int width = font->getStringWidth(lineStr.substr(0, i));
if (width > renderWidth)
break;
}
lineCommitted = i + 1;
}
prevWasWhitespace = isWhitespace;
}
if (lineCommitted > lineStr.size())
lineCommitted = lineStr.size();
// Too little space for anything
if (lineCommitted == 0) {
lineCommitted = 1;
for (size_t i = 2; i <= lineStr.size(); i++) {
int width = font->getStringWidth(lineStr.substr(0, i));
if (width > renderWidth)
break;
lineCommitted = i;
}
}
font->drawString(_renderedText.get(), lineStr.substr(0, lineCommitted), 0, line * height + (height - ascent) / 2, renderWidth, 1, textAlign, 0, false);
if (lineCommitted == lineStr.size())
lineStr.clear();
else {
lineStr = lineStr.substr(lineCommitted);
line++;
}
}
if (noMoreLines)
break;
line++;
lineStart = lineEndPos + 1;
}
}
Graphics::ManagedSurface *target = window->getSurface().get();
Common::Rect srcRect(0, 0, renderWidth, renderHeight);
Common::Rect destRect(_cachedAbsoluteOrigin.x, _cachedAbsoluteOrigin.y, _cachedAbsoluteOrigin.x + _rect.width(), _cachedAbsoluteOrigin.y + _rect.height());
// TODO: Need to handle more modes
const ColorRGB8 &color = _renderProps.getForeColor();
const uint8 drawPalette[2 * 3] = {
0, 0, 0,
color.r, color.g, color.b
};
if (_renderedText) {
_renderedText->setPalette(drawPalette, 0, 2);
target->transBlitFrom(*_renderedText.get(), srcRect, destRect, 0);
}
}
void TextLabelElement::setTextStyle(uint16 macFontID, const Common::String &fontFamilyName, uint size, TextAlignment alignment, const TextStyleFlags &styleFlags) {
if (!_text.empty()) {
_needsRender = true;
_contentsDirty = true;
}
_macFontID = macFontID;
_fontFamilyName = fontFamilyName;
_size = size;
_alignment = alignment;
_styleFlags = styleFlags;
}
Graphics::FontManager::FontUsage TextLabelElement::getDefaultUsageForMacFont(uint16 macFontID, uint size) {
switch (macFontID) {
case Graphics::kMacFontCourier:
return Graphics::FontManager::kConsoleFont;
default:
break;
}
warning("Unhandled font ID %i default, this might not render well", static_cast<int>(macFontID));
return Graphics::FontManager::kGUIFont;
}
Graphics::FontManager::FontUsage TextLabelElement::getDefaultUsageForNamedFont(const Common::String &fontFamilyName, uint size) {
if (fontFamilyName == "Courier New") {
if (size == 8)
return Graphics::FontManager::kConsoleFont;
} else if (fontFamilyName == "Arial") {
if (size == 10)
return Graphics::FontManager::kGUIFont;
if (size == 14)
return Graphics::FontManager::kBigGUIFont;
}
warning("Unhandled font name '%s' default, this might not render well", fontFamilyName.c_str());
return Graphics::FontManager::kGUIFont;
}
MiniscriptInstructionOutcome TextLabelElement::scriptSetText(MiniscriptThread *thread, const DynamicValue &value) {
if (value.getType() != DynamicValueTypes::kString) {
thread->error("Tried to set a text label element's text to something that wasn't a string");
return kMiniscriptInstructionOutcomeFailed;
}
_text = value.getString();
_needsRender = true;
_contentsDirty = true;
_macFormattingSpans.clear();
return kMiniscriptInstructionOutcomeContinue;
}
MiniscriptInstructionOutcome TextLabelElement::scriptSetLine(MiniscriptThread *thread, size_t lineIndex, const DynamicValue &value) {
DynamicValue derefValue = value.dereference();
if (derefValue.getType() != DynamicValueTypes::kString) {
thread->error("Tried to set a text label element's text to something that wasn't a string");
return kMiniscriptInstructionOutcomeFailed;
}
uint32 startPos;
uint32 endPos;
if (findLineRange(lineIndex, startPos, endPos))
_text = _text.substr(0, startPos) + derefValue.getString() + _text.substr(endPos, _text.size() - endPos);
else {
size_t numLines = countLines();
while (numLines <= lineIndex) {
_text += '\r';
numLines++;
}
_text += derefValue.getString();
}
_needsRender = true;
_contentsDirty = true;
_macFormattingSpans.clear();
return kMiniscriptInstructionOutcomeContinue;
}
bool TextLabelElement::findLineRange(size_t lineIndex, uint32 &outStartPos, uint32 &outEndPos) const {
uint32 lineStart = 0;
uint32 lineEnd = _text.size();
size_t linesToScan = lineIndex + 1;
while (linesToScan > 0) {
linesToScan--;
lineEnd = _text.find('\r', lineStart);
if (lineEnd == Common::String::npos) {
lineEnd = _text.size();
break;
}
}
if (linesToScan > 0)
return false;
outStartPos = lineStart;
outEndPos = lineEnd;
return true;
}
size_t TextLabelElement::countLines() const {
size_t numLines = 1;
for (char c : _text)
if (c == '\r')
numLines++;
return numLines;
}
MiniscriptInstructionOutcome TextLabelElement::TextLabelLineWriteInterface::write(MiniscriptThread *thread, const DynamicValue &dest, void *objectRef, uintptr ptrOrOffset) {
return static_cast<TextLabelElement *>(objectRef)->scriptSetLine(thread, ptrOrOffset, dest);
}
MiniscriptInstructionOutcome TextLabelElement::TextLabelLineWriteInterface::refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib) {
return kMiniscriptInstructionOutcomeFailed;
}
MiniscriptInstructionOutcome TextLabelElement::TextLabelLineWriteInterface::refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib, const DynamicValue &index) {
return kMiniscriptInstructionOutcomeFailed;
}
SoundElement::SoundElement()
: _leftVolume(0), _rightVolume(0), _balance(0), _assetID(0), _startTime(0), _finishTime(0), _cueCheckTime(0),
_startTimestamp(0), _shouldPlayIfNotPaused(true), _needsReset(true) {
}
SoundElement::~SoundElement() {
if (_playMediaSignaller)
_playMediaSignaller->removeReceiver(this);
}
bool SoundElement::load(ElementLoaderContext &context, const Data::SoundElement &data) {
if (!NonVisualElement::loadCommon(data.name, data.guid, data.elementFlags))
return false;
_paused = ((data.soundFlags & Data::SoundElement::kPaused) != 0);
_loop = ((data.soundFlags & Data::SoundElement::kLoop) != 0);
_leftVolume = data.leftVolume;
_rightVolume = data.rightVolume;
_balance = data.balance;
_assetID = data.assetID;
return true;
}
bool SoundElement::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
if (attrib == "loop") {
result.setBool(_loop);
return true;
} else if (attrib == "volume") {
result.setInt((_leftVolume + _rightVolume) / 2);
return true;
}
return NonVisualElement::readAttribute(thread, result, attrib);
}
MiniscriptInstructionOutcome SoundElement::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) {
if (attrib == "loop") {
DynamicValueWriteFuncHelper<SoundElement, &SoundElement::scriptSetLoop, true>::create(this, writeProxy);
return kMiniscriptInstructionOutcomeContinue;
} else if (attrib == "volume") {
DynamicValueWriteFuncHelper<SoundElement, &SoundElement::scriptSetVolume, true>::create(this, writeProxy);
return kMiniscriptInstructionOutcomeContinue;
} else if (attrib == "balance") {
DynamicValueWriteFuncHelper<SoundElement, &SoundElement::scriptSetBalance, true>::create(this, writeProxy);
return kMiniscriptInstructionOutcomeContinue;
} else if (attrib == "asset") {
DynamicValueWriteFuncHelper<SoundElement, &SoundElement::scriptSetAsset, true>::create(this, writeProxy);
return kMiniscriptInstructionOutcomeContinue;
}
return NonVisualElement::writeRefAttribute(thread, writeProxy, attrib);
}
VThreadState SoundElement::consumeCommand(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
if (Event(EventIDs::kPlay, 0).respondsTo(msg->getEvent())) {
StartPlayingTaskData *startPlayingTaskData = runtime->getVThread().pushTask("SoundElement::startPlayingTask", this, &SoundElement::startPlayingTask);
startPlayingTaskData->runtime = runtime;
return kVThreadReturn;
}
if (Event(EventIDs::kStop, 0).respondsTo(msg->getEvent())) {
StartPlayingTaskData *startPlayingTaskData = runtime->getVThread().pushTask("SoundElement::stopPlayingTask", this, &SoundElement::stopPlayingTask);
startPlayingTaskData->runtime = runtime;
return kVThreadReturn;
}
return NonVisualElement::consumeCommand(runtime, msg);
}
void SoundElement::activate() {
Project *project = getRuntime()->getProject();
Common::SharedPtr<Asset> asset = project->getAssetByID(_assetID).lock();
if (!asset) {
warning("Sound element references asset %i but the asset isn't loaded!", _assetID);
return;
}
if (asset->getAssetType() != kAssetTypeAudio) {
warning("Sound element assigned an asset that isn't audio");
return;
}
_cachedAudio = static_cast<AudioAsset *>(asset.get())->loadAndCacheAudio(getRuntime());
_metadata = static_cast<AudioAsset *>(asset.get())->getMetadata();
_playMediaSignaller = project->notifyOnPlayMedia(this);
if (_name.empty())
_name = project->getAssetNameByID(_assetID);
const SubtitleTables &subTables = project->getSubtitles();
if (subTables.assetMapping) {
const Common::String *subtitleSetIDPtr = subTables.assetMapping->findSubtitleSetForAssetID(_assetID);
if (!subtitleSetIDPtr) {
Common::String assetName = project->getAssetNameByID(_assetID);
if (assetName.size() > 0)
subtitleSetIDPtr = subTables.assetMapping->findSubtitleSetForAssetName(assetName);
}
if (subtitleSetIDPtr)
_subtitlePlayer.reset(new SubtitlePlayer(getRuntime(), *subtitleSetIDPtr, subTables));
}
}
void SoundElement::deactivate() {
if (_playMediaSignaller) {
_playMediaSignaller->removeReceiver(this);
_playMediaSignaller.reset();
}
_metadata.reset();
_cachedAudio.reset();
_player.reset();
}
bool SoundElement::canAutoPlay() const {
return !_paused;
}
void SoundElement::playMedia(Runtime *runtime, Project *project) {
if (_shouldPlayIfNotPaused) {
if (_paused) {
// Goal state is paused
// TODO: Track pause time
stopPlayer();
} else {
// Goal state is playing
if (_needsReset) {
// TODO: Reset to start time
stopPlayer();
_needsReset = false;
}
if (!_player) {
_finishTime = getRuntime()->getPlayTime() + _metadata->durationMSec;
int normalizedVolume = (_leftVolume + _rightVolume) * 255 / 200;
int normalizedBalance = _balance * 127 / 100;
// TODO: Support ranges
size_t numSamples = _cachedAudio->getNumSamples(*_metadata);
_player.reset(new AudioPlayer(getRuntime()->getAudioMixer(), normalizedVolume, normalizedBalance, _metadata, _cachedAudio, _loop, 0, 0, numSamples));
_startTime = runtime->getPlayTime();
_cueCheckTime = _startTime;
_startTimestamp = 0;
}
uint64 newTime = getRuntime()->getPlayTime();
if (newTime > _cueCheckTime) {
uint64 oldTimeRelative = _cueCheckTime - _startTime + _startTimestamp;
uint64 newTimeRelative = newTime - _startTime + _startTimestamp;
if (_subtitlePlayer)
_subtitlePlayer->update(oldTimeRelative, newTimeRelative);
for (MediaCueState *mediaCue : _mediaCues)
mediaCue->checkTimestampChange(runtime, oldTimeRelative * _metadata->sampleRate / 1000u, newTimeRelative * _metadata->sampleRate / 1000u, true, true);
_cueCheckTime = newTime;
}
if (!_loop && newTime >= _finishTime) {
// Don't throw out the handle - It can still be playing but we just treat it like it's not.
// If it has anything left, then we let it finish and avoid clipping the sound, but we need
// to know that the handle is still here so we can actually stop it if the element is
// destroyed, since the stream is tied to the CachedAudio.
Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event(EventIDs::kStop, 0), DynamicValue(), getSelfReference()));
Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
runtime->queueMessage(dispatch);
_shouldPlayIfNotPaused = false;
if (_subtitlePlayer)
_subtitlePlayer->stop();
}
}
} else {
// Goal state is stopped
stopPlayer();
}
}
bool SoundElement::resolveMediaMarkerLabel(const Label &label, int32 &outResolution) const {
if (_metadata) {
for (const AudioMetadata::CuePoint &cuePoint : _metadata->cuePoints) {
if (cuePoint.cuePointID == label.id) {
outResolution = cuePoint.position;
return true;
}
}
}
return false;
}
void SoundElement::stopPlayer() {
_player.reset();
if (_subtitlePlayer)
_subtitlePlayer->stop();
}
#ifdef MTROPOLIS_DEBUG_ENABLE
void SoundElement::debugInspect(IDebugInspectionReport *report) const {
NonVisualElement::debugInspect(report);
report->declareDynamic("leftVol", Common::String::format("%i", _leftVolume));
report->declareDynamic("rightVol", Common::String::format("%i", _rightVolume));
report->declareDynamic("balance", Common::String::format("%i", _balance));
report->declareDynamic("asset", Common::String::format("%i", _assetID));
AudioMetadata *metadata = _metadata.get();
report->declareDynamic("duration", metadata ? Common::String::format("%i", metadata->durationMSec) : Common::String("Unknown"));
report->declareDynamic("finishTime", Common::String::format("%i", static_cast<int>(_finishTime)));
report->declareDynamic("shouldPlayIfNotPaused", _shouldPlayIfNotPaused ? "true" : "false");
report->declareDynamic("paused", _paused ? "true" : "false");
report->declareDynamic("needsReset", _needsReset ? "true" : "false");
}
#endif
MiniscriptInstructionOutcome SoundElement::scriptSetLoop(MiniscriptThread *thread, const DynamicValue &value) {
if (value.getType() != DynamicValueTypes::kBoolean)
return kMiniscriptInstructionOutcomeFailed;
setLoop(value.getBool());
return kMiniscriptInstructionOutcomeContinue;
}
MiniscriptInstructionOutcome SoundElement::scriptSetVolume(MiniscriptThread *thread, const DynamicValue &value) {
int32 asInteger = 0;
if (!value.roundToInt(asInteger))
return kMiniscriptInstructionOutcomeFailed;
if (asInteger < 0)
asInteger = 0;
else if (asInteger > 100)
asInteger = 100;
setVolume(asInteger);
return kMiniscriptInstructionOutcomeContinue;
}
MiniscriptInstructionOutcome SoundElement::scriptSetBalance(MiniscriptThread *thread, const DynamicValue &value) {
int32 asInteger = 0;
if (!value.roundToInt(asInteger))
return kMiniscriptInstructionOutcomeFailed;
if (asInteger < -100)
asInteger = -100;
else if (asInteger > 100)
asInteger = 100;
setBalance(asInteger);
return kMiniscriptInstructionOutcomeContinue;
}
MiniscriptInstructionOutcome SoundElement::scriptSetAsset(MiniscriptThread *thread, const DynamicValue &value) {
DynamicValue derefValue = value.dereference();
if (derefValue.getType() != DynamicValueTypes::kString) {
thread->error("Tried to set a sound element's asset to something that wasn't a string");
return kMiniscriptInstructionOutcomeFailed;
}
stopPlayer();
Project *project = thread->getRuntime()->getProject();
uint32 assetID = 0;
if (!project->getAssetIDByName(derefValue.getString(), assetID)) {
warning("Sound element references asset '%s' but the asset ID couldn't be resolved", derefValue.getString().c_str());
return kMiniscriptInstructionOutcomeFailed;
}
Common::Array<Common::SharedPtr<Asset> > forceLoadedAssets;
Common::SharedPtr<Asset> asset = project->getAssetByID(assetID).lock();
if (!asset) {
if (thread->getRuntime()->getHacks().allowAssetsFromOtherScenes) {
project->forceLoadAsset(assetID, forceLoadedAssets);
asset = project->getAssetByID(assetID).lock();
}
}
if (!asset) {
warning("Sound element references asset '%s' but the asset isn't loaded!", derefValue.getString().c_str());
return kMiniscriptInstructionOutcomeFailed;
}
if (asset->getAssetType() != kAssetTypeAudio) {
warning("Sound element assigned an asset that isn't audio");
return kMiniscriptInstructionOutcomeFailed;
}
_cachedAudio = static_cast<AudioAsset *>(asset.get())->loadAndCacheAudio(getRuntime());
_metadata = static_cast<AudioAsset *>(asset.get())->getMetadata();
_assetID = asset->getAssetID();
return kMiniscriptInstructionOutcomeContinue;
}
void SoundElement::setLoop(bool loop) {
_loop = loop;
}
void SoundElement::setVolume(uint16 volume) {
uint16 fullVolumeLeft = 100 - _balance;
// Weird math to ensure _leftVolume + _rightVolume stays divisible by 2
_leftVolume = (volume * fullVolumeLeft + 50) / 100;
_rightVolume = volume * 2 - _leftVolume;
}
void SoundElement::setBalance(int16 balance) {
_balance = balance;
setVolume((_leftVolume + _rightVolume) / 2);
}
VThreadState SoundElement::startPlayingTask(const StartPlayingTaskData &taskData) {
// Pushed in reverse order, actual order is Unpaused -> Played
{
Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event(EventIDs::kPlay, 0), DynamicValue(), getSelfReference()));
Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
taskData.runtime->sendMessageOnVThread(dispatch);
}
if (_paused) {
Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event(EventIDs::kUnpause, 0), DynamicValue(), getSelfReference()));
Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
taskData.runtime->sendMessageOnVThread(dispatch);
_paused = false;
}
_shouldPlayIfNotPaused = true;
_needsReset = true;
return kVThreadReturn;
}
VThreadState SoundElement::stopPlayingTask(const StartPlayingTaskData &taskData) {
if (_shouldPlayIfNotPaused) {
Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event(EventIDs::kStop, 0), DynamicValue(), getSelfReference()));
Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
taskData.runtime->sendMessageOnVThread(dispatch);
_shouldPlayIfNotPaused = false;
_needsReset = true;
}
return kVThreadReturn;
}
} // End of namespace MTropolis